单例模式的持续改进

Posted WXCOP无限靠谱

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式的持续改进相关的知识,希望对你有一定的参考价值。

引言


单例模式是我们在日常开发中使用比较多的一种设计模式,它主要是保证系统中有且仅有一个对象实例存在,这样做的好处是:不必每次都创建一个对象实例,减少浪费。



传统单例模式的写法与问题


早在阎宏的《Java与模式》一书中提到了饿汉式与懒汉式单例模式的写法。

  • 饿汉式单例模式写法

    1. public class Singleton {

    2. private static Singleton instance = new Singleton();

    3. private Singleton(){}

    4. public static Singleton getInstance() {

    5.    return instance;

    6. }

    7. }

  • 懒汉式单例模式写法

    1. public class Singleton {

    2. private static Singleton instance;

    3. private Singleton() {}

    4. public synchronized static Singleton getInstance() {

    5.    if (instance == null) {

    6.        instance = new Singleton();

    7.    }

    8.    return instance;

    9. }

    10. }

  • 存在问题

饿汉式单例存在的问题是在类加载时就会初始化创建对象,而没有等到真正使用对象时才创建,存在一些小小的浪费。 懒汉式单例虽然实现了延迟加载,但存在的问题主要在并发上。虽然加了synchronized关键字,但这个锁是在整个对象实例上的,所以效率牺牲比较大。



饿汉式单例模式的优化


如果要避免在初始化类的时候就创建对象,可以把对象封装在内部类中,随方法创建而创建。

 
   
   
 
  1. public class BetterSingleton {

  2.    private BetterSingleton() {}

  3.    public static BetterSingleton getInstance() {

  4.        return SingletonHolder.instance;

  5.    }

  6.    private static class SingletonHolder {

  7.        private static BetterSingleton instance = new BetterSingleton();

  8.    }

  9. }



懒汉式单例模式的优化


优化并发的一个原则就是减少锁的持有时间,所以很常见的一个写法就是“双重加锁机制”:

 
   
   
 
  1. public class DoubleLockSingleton {

  2.    private static DoubleLockSingleton instance;

  3.    private DoubleLockSingleton() {

  4.    }

  5.    public static DoubleLockSingleton getInstance() {

  6.        if (instance == null) {

  7.            synchronized (DoubleLockSingleton.class) {

  8.                if (instance == null) {

  9.                    instance = new DoubleLockSingleton();

  10.                }

  11.            }

  12.        }

  13.        return instance;

  14.    }

  15. }


但这里还有一个小小的问题,就是并发中的重排序问题,现代CPU为了加速指令的执行,常常通过重排序技术达到效果。比如这里的new DoubleLockSingleton()操作就不是一个原子操作,实际上会分解成下面3步:

  1. 给instance分配内存,在栈中分配并初始化为null;

  2. 调用Singleton的构造函数,生成对象实例,在堆中分配;

  3. 把instance指向在堆中分配的对象。


由于指令重排序优化,执行顺序可能会变成1,3,2,那么当一个线程执行完1,3之后,被另一个线程抢占,这时instance已经不是null了,就会直接返回。然而2还没有执行过,也就是说这个对象实例还没有初始化过。 解决方法就是在实例变量前加上volatile修饰符,防止对象重排序。更新后的代码如下:

 
   
   
 
  1. public class DoubleLockSingleton {

  2.    private volatile static DoubleLockSingleton instance;

  3.    private DoubleLockSingleton() {

  4.    }

  5.    public static DoubleLockSingleton getInstance() {

  6.        if (instance == null) {

  7.            synchronized (DoubleLockSingleton.class) {

  8.                if (instance == null) {

  9.                    instance = new DoubleLockSingleton();

  10.                }

  11.            }

  12.        }

  13.        return instance;

  14.    }

  15. }``

注意:volatile关键字在JDK1.5版本后才支持。



使用Java中的enum创建单例


说了这么多,其实在《Efective Java》这本书中,Bloch还介绍了一种很好的创建单例的方法—就是利用enum特性:enum会天然保证创建出来的对象是单例且是同步的。

 
   
   
 
  1. public enum EnumSingleton {

  2.    INSTANCE;

  3.    public static EnumSingleton getInstance() {

  4.        return INSTANCE;

  5.    }

  6. }

应该说,这是最优雅的一种方式了吧! :)



观点碰撞1


石墨嘻最后一种是最优雅的一种,但却有个致命的弱点:具有不可测试性,因为 instance 无法被替换。


丁一enum单例,利用强大的powermock,还是能测的。下面是一个例子: