Head First 设计模式(五)单件模式

定义

单件模式确保一个类只有一个实例,并提供一个全局访问点

所谓单件模式,其实就是我们常说的单例模式。

对于线程池、缓存、日志对象等全局只需要一个对象,适合用单件模式来产生。

几种实现

1. 经典单例模式(懒汉式创建)

/**
 * 经典单件模式实现
 */
public class Singleton {
    private static Singleton singleton;
    //私有构造器,使单件类只能通过getInstance()获取
    private Singleton() {

    }
    //保证singleton的唯一实例
    public static Singleton getInstance(){
        if(singleton == null)
            singleton = new Singleton();
        return singleton;
    }
}

经典的单例模式,教科书上的Demo代码。但这种写法只是用来讲解,实际运用是有问题的(线程不安全)。

需要注意的是,对象只有在需要时(调用getInstance()方法)才会创建。所以这种模式又被称为“懒汉式”单例模式

这种延迟实例化的优势在于:对于非常耗费资源的对象,如果程序一开始就创建了它,然后执行过程中一直没使用,会造成浪费。

2. 多线程单例模式(懒汉式创建)

如果我们细究第一种的单例模式,会发现它是线程不安全的。在多线程环境下,单例对象第一次创建时,可能被多个线程重复创建对象

if(singleton == null)
      singleton = new Singleton();

如何解决?最简单的方式是给全局获取单例的方法getInstance()加锁:

/**
 * 单件模式加锁实现,保证了单件模式的线程安全
 */
public class SynSingleton {
    private static SynSingleton singleton;
    //私有构造器,使单件类只能通过getInstance()获取
    private SynSingleton() {

    }
    //保证singleton的唯一实例,加上synchronized保证该方法被调用时的线程安全
    public static synchronized SynSingleton getInstance(){
        if(singleton == null)
            singleton = new SynSingleton();
        return singleton;
    }
}

这样,单例模式便线程安全了。

但是,我们再思考一下,这个模式仍然有许多问题。首先,加锁对性能的影响很大。而我们其实只用保证单例模式在第一次执行该方法时线程安全。

也就是说,一旦singleton被创建出来,该方法是不再需要同步的,而现在,每次我们调用getInstance()方法时,都是同步的。

3. 多线程单例模式(饿汉式创建)

如何解决上一种单例模式实现的弊端?

很简单,既然第一次创建单例时会造成线程安全,那我最开始就创建出单例对象就好了:

/**
 * “急创建”单件模式实现,静态变量初始化保证了单件模式的线程安全
 */
public class StaticInitializeSingleton {
    private static StaticInitializeSingleton singleton = new StaticInitializeSingleton();
    //私有构造器,使单件类只能通过getInstance()获取
    private StaticInitializeSingleton() {

    }
    //保证singleton的唯一实例,加上synchronized保证该方法被调用时的线程安全
    public static synchronized StaticInitializeSingleton getInstance(){
        return singleton;
    }
}

这种初始化类时便创建单例对象被称为“饿汉式”单例模式,大部分情况使用这种写法就行了。

但是,我们发现先前写法里我们引以为傲的“延迟实例化”没了,对于十分耗资源的对象,在程序启动的一刻就创建,然后一直没有用,无疑是会对性能造成影响的。

那么有没有更完美的单例模式呢?

4. 多线程单例模式(双重检查加锁)

/**
 * 双重检查加锁单件模式实现
 */
public class DoubleCheckSingleton {
    private volatile static DoubleCheckSingleton singleton;
    //私有构造器,使单件类只能通过getInstance()获取
    private DoubleCheckSingleton() {

    }
    //保证singleton的唯一实例
    public static synchronized DoubleCheckSingleton getInstance(){
        if(singleton == null){
            synchronized (DoubleCheckSingleton.class) {
                if(singleton == null)
                    singleton = new DoubleCheckSingleton();
            }
        }
        return singleton;
    }
}

这种双重加锁检查的单例模式,即保证了只有在第一次创建单例时加锁,又保证了延迟实例化。

值得注意的是:

  1. 加锁的代码里面继续判断了singleton == null,这是为了防止实例化完成后,先前已经进入if(singleton == null)代码块的线程,再次创建单例。
  2. volatile 关键字必须使用,原因在与volatile除了保证线程之间的可见性外,还可以禁止指令重排序优化

此处直接引用你真的了解volatile关键字吗?中的解释:

为什么要使用volatile修饰instance

主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:

1.给 instance分配内存

2.调用Singleton 的构造函数来初始化成员变量

3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)。

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

最后,由于valatile关键字出现在JDK1.4之后,所以,该模式不适用于JDK1.4之后的版本。

那,有没有兼容所有JDK的更完美的写法了?

5. 终极解决方案:静态内部类单例模式

public class Singleton {  
    private static class SingletonHolder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE; 
    }  
}

这种写法是《Effective Jave》里面推荐的写法。

这种写法使用JVM的机制来保证了线程安全。只有第一次执行return SingletonHolder.INSTANCE代码时,静态内部类进行初始化,才会实例化单例,从而实现了“延迟实例化”。


本文总结自:
《Head First设计模式》 第五章:单件模式

参考文章:
如何正确地写出单例模式
你真的了解volatile关键字吗?

猜你喜欢

转载自blog.csdn.net/z55887/article/details/69668743