双重检验锁思考

最近在项目中写一个池子,用到了双重检验锁,联想到单例模式的双重检验锁。

1 单例模式

下面是一个懒加载的单例模式

public class Singleton {
    private volatile Singleton instance = null; // volatile禁止指令重排序

    private Singleton() {

    }

    public static Singleton getInstance() {
        if (instance == null) { // 1. 减少锁粒度,避免不必要的加锁
            synchronized (Singleton.class) {
                if (instance == null) { //  2. 获取锁后要再次检验状态,以为状态可能被其他线程改变
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

需要判断两次instace == null是因为

  1. 第一次为了减少锁了粒度,因为只有在instance == null的时候才需要上锁,其他情况可以直接返回
  2. 第二次获取锁后还需要判断 instance == null是因为instance可能已经被改变,所以要再次判断。例如:两个线程都在等待获取锁。线程A获取到后实例化了instance后释放了锁,instance现在不是null;之后线程B获取到锁,如果不判断instance == null的话便会又重新创建一个instance。

其实加锁的原因是判断null和new对象需要是个原子操作,而在锁之外判断null的原因是减少锁的粒度。

因此我们可以总结对于类似情况的锁使用

  • 在需要的时候才上锁,减少锁粒度,减少锁竞争
  • 获取锁后要判断状态,因为其他线程可能会改变状态

2 volatile

另外注意到,类的属性使用了volatile。先写下结论:这里的volatile是为了禁止重排序,而不是可见性。

原因是:new一个对象其实不是原子的,需要3个步骤

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

jvm存在指令重排序优化,有可能上述步骤变为1-3-2。假设现在的执行顺序是1-3-2,现在有两个线程A和B。A获取锁后,执行new对象,执行步骤是1-3,还未执行2。需要注意的是,执行完3后jinstance就未非null了,而第2步还没有执行,对象不是完整的对象,此时如果判断instance == null 将返回false。此时线程B执行判断同步代码块外的 instance == null 判断(即1. 减少锁粒度,避免不必要的加锁 这个判断),得到结果false,直接返回了这个不完整的对象。因此这里volatile是为了避免指令重排序,而不是可见性。

3 参考

http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/
https://blog.csdn.net/xiakepan/article/details/52444565

原文地址:https://www.cnblogs.com/set-cookie/p/8813619.html