单例双重加锁

  相信面向对象程序员都对单例模式比较熟悉,而对于单例模式却有着各种各样的写法,今天我专门针对一种称为双重加锁的写法进行分析。我们先来看下这种写法。

/**
 * 单例双重加锁Demo
 *
 */
public class DoubleCheckLock {
   
    private static DoubleCheckLock instance ;
    
    private DoubleCheckLock(){
        
    }
    
    public static DoubleCheckLock getInstance(){
        if(instance == null){
            synchronized (DoubleCheckLock.class) {
                if(instance == null)
                    instance = new DoubleCheckLock() ;
            }
        }
        return instance;
    }
}

  这种写法相信很多人都见过,但是你认为这种写法是正确的吗?或者更准确的来说,这种写法在并发的环境下是否还能表现出正确的行为呢。

  之所以有这种所谓的双重加锁,一方面是因为延迟初始化可以提高性能,另一方面通过使用内置锁sychronized来防止并发,其原理是首先检查是否在没有同步的情况下进行了初始化,如果没有的话,在进行同步,然后再次检查是否对其(instance)进行了初始化,如果没有那么则初始化DoubleCheckLock。

  这种写法表面看起来既提高了性能,又保证了线程安全。但实际上却并不是如此,我只从线程安全上来分析这种写法的对错。

  在这,首先应该注意的是使用内置锁加锁的是DoubleCheckLock.class,并不是instance,也就是说没有在instance实现同步,那么在这种情况下,当有两个线程同时进行到synchronized代码块时,只有一个线程可以进入,然后初始化了instance,但是这仅仅只能保证的是两个线程在访问上的独占性,也就是说两个线程在此一定是一先一后进行访问,但是不能保证的是instance的内存可见性,原因很简单,因为同步的对象并不是instance,而是DoubleCheckLock.class(可以保证内存可见性)。不能保证内存可见性的后果就是当第一个线程初始化instance之后,第二个线程并不能马上看见instance被初始化,或者更准确的来说,第二个线程看到的可能只是被部分构造的instance。因此,这种造成的后果是第二个线程读取到了错误的instance的状态,有可能instance会被再次实例化。

  那么如何解决这个问题呢,最简单的方式是对instance加上关键词volatile,volatile可以保证变量的内存可见性,同时volatile同步的消耗也非常小,这么做到话,可以保证线程安全。

  上述解决问题的方式固然是可以,但是实质上我感觉很繁琐其代码阅读效果也不好,就单例而言,我推荐一下的写法。

public class Single {
    
    private Single(){}
    
    private static class SingleHolder{
        public static Single instance = new Single();
    }
    
    public static Single getInstance(){
        return SingleHolder.instance;
    }
}

  这种写法相对而言比较简单,而且处理了两个问题:1.线程安全问题。2.延迟初始化(初始化在调用getInstance的时候才会去静态内部类中初始化instance)。而且相对而言,有着更加良好的代码可读性。

  对于双重加锁的这种写法就先分析到这,等后面说到Happens-Before之后我会再来分下下双重加锁。

 

posted @ 2017-01-13 10:46  言枫  阅读(6473)  评论(0编辑  收藏  举报