一个简单的单例示例

来源:互联网 发布:python pdf转html代码 编辑:程序博客网 时间:2024/06/06 02:32

一个简单的单例示例

单例模式可能是大家经常接触和使用的一个设计模式,你可能会这么写

public class UnsafeLazyInitiallization {    private static UnsafeLazyInitiallization instance;    private UnsafeLazyInitiallization() {    }    public static UnsafeLazyInitiallization getInstance(){        if(instance==null){   //1:A线程执行            instance=new UnsafeLazyInitiallization();  //2:B线程执行        }        return instance;    }}

上面代码大家应该都知道,所谓的线程不安全的懒汉单例写法。在UnsafeLazyInitiallization类中,假设A线程执行代码1的同时,B线程执行代码2,此时,线程A可能看到instance引用的对象还没有初始化。

你可能会说,线程不安全,我可以对getInstance()方法做同步处理保证安全啊,比如下面这样的写法

 public class SafeLazyInitiallization {        private static SafeLazyInitiallization instance;        private SafeLazyInitiallization() {        }        public synchronized static SafeLazyInitiallization getInstance(){            if(instance==null){                instance=new SafeLazyInitiallization();            }            return instance;        }    }

这样的写法是保证了线程安全,但是由于getInstance()方法做了同步处理,synchronized将导致性能开销。如getInstance()方法被多个线程频繁调用,将会导致程序执行性能的下降。反之,如果getInstance()方法不会被多个线程频繁的调用,那么这个方案将能够提供令人满意的性能。

那么,有没有更优雅的方案呢?前人的智慧是伟大的,在早期的JVM中,synchronized存在巨大的性能开销,因此,人们想出了一个“聪明”的技巧——双重检查锁定。人们通过双重检查锁定来降低同步的开销。下面来让我们看看

public class DoubleCheckedLocking { //1    private static DoubleCheckedLocking instance; //2    private DoubleCheckedLocking() {    }    public static DoubleCheckedLocking getInstance() { //3        if (instance == null) { //4:第一次检查            synchronized (DoubleCheckedLocking.class) { //5:加锁                if (instance == null) //6:第二次检查                    instance = new DoubleCheckedLocking(); //7:问题的根源出在这里            } //8        } //9        return instance; //10    } //11}

如上面代码所示,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此,可以大幅降低synchronized带来的性能开销。双重检查锁定看起来似乎很完美,但这是一个错误的优化!为什么呢?在线程执行到第4行,代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。在第7行创建了一个对象,这行代码可以分解为如下的3行伪代码

memory=allocate(); //1:分配对象的内存空间ctorInstance(memory); //2:初始化对象instance=memory; //3:设置instance指向刚分配的内存地址

上面3行代码中的2和3之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实发生的,如果不了解重排序,后文JMM会详细解释)。2和3之间重排序之后的执行时序如下

memory=allocate(); //1:分配对象的内存空间instance=memory; //3:设置instance指向刚分配的内存地址,注意此时对象还没有被初始化ctorInstance(memory); //2:初始化对象

回到示例代码第7行,如果发生重排序,另一个并发执行的线程B就有可能在第4行判断instance不为null。线程B接下来将访问instance所引用的对象,但此时这个对象可能还没有被A线程初始化。在知晓问题发生的根源之后,我们可以想出两个办法解决

  • 不允许2和3重排序
  • 允许2和3重排序,但不允许其他线程“看到”这个重排序

下面就介绍这两个解决方案的具体实现

基于volatile的解决方案

对于前面的基于双重检查锁定的方案,只需要做一点小的修改,就可以实现线程安全的延迟初始化。请看下面的示例代码

public class SafeDoubleCheckedLocking {    private volatile static SafeDoubleCheckedLocking instance;    private SafeDoubleCheckedLocking() {    }    public static SafeDoubleCheckedLocking getInstance() {        if (instance == null) {            synchronized (SafeDoubleCheckedLocking.class) {                if (instance == null)                    instance = new SafeDoubleCheckedLocking();//instance为volatile,现在没问题了            }        }        return instance;    }}

当声明对象的引用为volatile后,前面伪代码谈到的2和3之间的重排序,在多线程环境中将会被禁止。

原创粉丝点击