设计模式-单例模式

Author Avatar
晓晓Sama 3月 24, 2018

意图

保证一个类仅有一个实例,并提供一个访问它的全局访问点。
我们怎么样才能保证一个类只有一个实例并且这个实例易于被访问呢?
如果将对象赋值给一个java静态变量,那么你必须在程序一开始就创建好对象。万一这个对象非常耗费资源,>而程序在这次的执行过程中又一直没有使用到它,不就形成浪费吗?
一个更好的办法是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建(通过截取创建新对象的请求) ,并且它可以提供一个访问该实例的方法。这就是S i n g l e t o n模式,我们可以在需要时才创建对象。
在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。

单例模式的实现

第一种:懒汉,线程不安全##

public class LazyNotSecurtySingleton {
    //使用静态变量来记录Singleton类的唯一实例
    private static LazyNotSecurtySingleton instance;
    // 私有构造,确保只有自类内部才能访问
    private LazyNotSecurtySingleton() {
    }
    //返回该类的实例, 有线程同步问题    
    public static  LazyNotSecurtySingleton getInstance() {
        if (instance == null) {
            instance = new LazyNotSecurtySingleton();
        }
        return instance;
    }
}

Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。(事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效。此问题在此处不做讨论,姑且掩耳盗铃地认为反射机制不存在。)
但是以上实现没有考虑线程安全问题。所谓线程安全是指:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。显然以上实现并不满足线程安全的要求,在并发环境下很可能出现多个Singleton实例。

第二种:懒汉 线程安全

public class LazySecurtySingleton {

    private static LazySecurtySingleton instance;
    /**
     * 私有构造子,确保无法在类外实例化该类
     */
    private LazySecurtySingleton() {
    }
    /**
     * synchronized关键字解决多个线程的同步问题
     */
    public static synchronized LazySecurtySingleton getInstance() {
        if (instance == null) {
            instance = new LazySecurtySingleton();
        }
        return instance;
    }
}

静态工厂方法中synchronized关键字提供的同步是必须的,否则当多个线程同时访问该方法时,无法确保获得的总是同一个实例。然而我们也看到,在所有的代码路径中,虽然只有第一次引用的时候需要对instance变量进行实例化,但是synchronized同步机制要求所有的代码执行路径都必须先获取类锁。在并发访问比较低时,效果并不显著,但是当并发访问量上升时,这里有可能会成为并发访问的瓶颈。

第三种:饿汉式

public class EagerSingleton {  
    //私有的类成员常量  
    private static final EagerSingleton SINGLETON = new EagerSingleton();  
    //私有的默认构造方法,此类不能被继承  
    private EagerSingleton(){}  
    //静态工厂方法  
    public static EagerSingleton getInstance(){  
        return SINGLETON;  
    }  

}  

这种方式,我们依赖JVM在加载这个类时马上创建该类的唯一实例,避免了线程安全问题。不过,instance在类装载时就实例化,没有达到lazy loading的效果.

第四种:双重校验锁

可以使用“双重检查加锁”的方式来实现,就可以既实现线程安全,又能够使性能不受很大的影响。那么什么是“双重检查加锁”机制呢?
所谓“双重检查加锁”机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,
进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
“双重检查加锁”机制的实现会使用关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
注意:在java1.4及以前版本中,很多JVM对于volatile关键字的实现的问题,会导致“双重检查加锁”的失败,因此“双重检查加锁”机制只能用在java5及以上的版本。

public class TwoLockSingleton {
    private volatile static TwoLockSingleton singleton;
    private TwoLockSingleton() {
    }
    public static TwoLockSingleton getInstance() {
        //先检查实例是否存在,如果不存在才进入下面的同步块
        if (singleton == null) {
            //同步块,线程安全的创建实例
            synchronized (TwoLockSingleton.class) {
                //再次检查实例是否存在,如果不存在才真正的创建实例
                if (singleton == null) {
                    singleton = new TwoLockSingleton();
                }
            }
        }
        return singleton;
    }
}

这种实现方式既可以实现线程安全地创建实例,而又不会对性能造成太大的影响。它只是第一次创建实例的时候同步,以后就不需要同步了,从而加快了运行速度。
提示:由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。
因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。
根据上面的分析,常见的两种单例实现方式都存在小小的缺陷,那么有没有一种方案,既能实现延迟加载,又能实现线程安全呢?

第五种:静态内部类

这个模式综合使用了Java的类级内部类和多线程缺省同步锁的知识,很巧妙地同时实现了延迟加载和线程安全。

1. 相应的基础知识

什么是类级内部类

  • 简单点说,类级内部类指的是,有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。
  • 类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。
  • 类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量。
  • 类级内部类相当于其外部类的成员,只有在第一次被使用的时候才被会装载。

多线程缺省同步锁的知识
大家都知道,在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况中,JVM已经隐含地为您执行了同步,这些情况下就不用自己再来进行同步控制了。
这些情况包括:

  • 由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
  • 访问final字段时
  • 在创建线程之前创建对象时
  • 线程可以看见它将要处理的对象时

2. 解决方案的思路

要想很简单地实现线程安全,可以采用静态初始化器的方式,它可以由JVM来保证线程的安全性。比如前面的饿汉式实现方式。但是这样一来,不是会浪费一定的空间吗?因为这种实现方式,会在类装载的时候就初始化对象,不管你需不需要。
如果现在有一种方法能够让类装载的时候不去初始化对象,那不就解决问题了?一种可行的方式就是采用类级内部类,在这个类级内部类里面去创建对象实例。这样一来,只要不使用到这个类级内部类,那就不会创建对象实例,从而同时实现延迟加载和线程安全。

/**
 * Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,
 * 只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。
 */
public class StaticInternalSingleton {
    /** 
     * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例没有绑定关系
     * 而且只有被调用到时才会装载,从而实现了延迟加载。
     */
    private static class SingletonHolder {
        /**
         * 静态初始化器,由JVM来保证线程安全
         */
        private final static StaticInternalSingleton INSTANCE = new StaticInternalSingleton();
    }
    private StaticInternalSingleton() {
    }
    public static StaticInternalSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

JVM将推迟SingletonHolder 类的初始化,直到当getInstance方法第一次被调用的时候,它第一次读取SingletonHolder.instance,导致SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。
这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本

第六种:枚举

/**
 * 避免多线程同步问题,而且还能防止反序列化重新创建新的对象
 */
public enum EnumSingleton {
    INSTANCE;
    public void whateverMethod() {
    }
}

这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式可以被继承的单例类,登记式单例类是为了克服饿汉式单例类和懒汉式单例类不可继承的缺点而设计的。

import java.util.HashMap;  

public class RegSingleton {  
    private static HashMap registry=new HashMap();  
    /**
     * 静态代码块 
     * 静态代码块优先于主方法执行,而在类中定义的静态代码会优先于构造块执行,而且不管产生多少对象,静态代码块只执行一次。 
     */  
    static{  
        RegSingleton singleton=new RegSingleton();  
        registry.put(singleton.getClass().getName(), singleton);  
    }  

    protected RegSingleton(){}  

    public static RegSingleton getInstance(String name){  
        if(name==null){  
            name="com.zzs.singleton.RegSingleton";  
        }  
        if(registry.get(name)==null){  
            try {  
                registry.put(name, Class.forName(name).newInstance());  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        }  
        return (RegSingleton) registry.get(name);  
    }  

}  

java代码

public class RegSingletonChild extends RegSingleton {  
    //由于子类必须允许父类以构造方法调用产生实例,所以它的构造方法必须是公开的,protected或public  
    protected RegSingletonChild() {  
    }  
    //静态方法工厂  
    public static RegSingletonChild getInstance() {  
        return (RegSingletonChild) RegSingleton  
                .getInstance("com.zzs.singleton.RegSingletonChild");  
    }  
}  

Spring singleton

Spring容器最初提供了两种bean的scope类型:singleton和prototype,但发布2.0之后,又引入了另外三种scope类型,即request,session和global session类型。不过这三种类型有所限制,只能在web应用中使用,也就是说,只有在支持web应用的ApplicationContext中使用这三个scope才是合理的

beans---org.springframework.beans.factory.support.AbstractBeanFactory/doGetBean ---singleton---prototype
web---org.springframework.web.context.request.ServletRequestAttributes---request---session---global session