浅谈双重检查锁定和延迟初始化

在Java多线程程序中,有时需要采用延迟初始化来降低初始化类和创建对象的开销,双重检查锁定是常见的延迟初始化技术,但它是一种错误的用法

双重检查锁的演进以及问题
  • 使用syncronized实现
public synchronized static Instance getInstance() {
    if (instance == null) {
        instance = new Instance();
    }
    return instance;
}

多线程情况下性能开销比较大。

  • 非线程安全的双重检查锁
public static Instance getInstance2() {
    if (instance2 == null) {
        synchronized (UnsafeLazyInitialization.class) {
            if (instance2 == null) {
                instance2 = new Instance();
            }
        }
    }
    return instance2;
}

这里看起来很完美,但是是一个错误的优化,代码在读取到instance2不为null的时候,instance引用的对象有可能换没有完成初始化,这样返回的instance2是有问题的。
出现这个问题的根源在什么地方?
instance2 = new Instance();这一行代码在处理器执行的时候有三部操作:
1、memory = allocate() //分配内存
2、ctorInstance(memory) //初始化对象
3、instance = memory //设置instance指向刚刚分配的内存地址

上面的三行代码中,2和3之间可能会被指令重排序。如果重排序之后的顺序为1,3,2.线程A执行2的时候,线程A判断instance2不为空,返回的instance2对象就是一个还未初始化的对象。

所以对于上面的解决思路有两种:
1、不允许2和3进行指令的重排序
3、允许2和3重排序,但是不允许其他线程看到这个重排序。

  • 不允许2和3进行指令重排序(线程安全的双重检查锁)
/**声明为volatile之后,2和3的指令重排序会被禁止*/
public static volatile Instance instance3;
public static Instance getInstance3() {
    if (instance3 == null) {
        synchronized (UnsafeLazyInitialization.class) {
            if (instance3 == null) {
                instance3 = new Instance();
            }
        }
    }
    return instance3;
}

-基于类初始化的解决

public class InstanceFactory {
    
    private static class InstanceHolder {
        public static Instance instance = new Instance();
    }

    public static Instance getInstance () {
        return InstanceHolder.instance;
    }
}

这个是基于JVM的特性:JVM在类初始化的时候,会执行类初始化,在执行类初始化期间,JVM会获取一把锁,这个锁可以同步多个线程对同一个类的初始化。初始化一个类,包括执行这个类的静态初始化和初始化这个类中的静态字段。根据Java语言规范,在首次发生下面的任何一种情况,一个类或接口类型将立即被初始化。
1)T的实例类型被创建
2)T是一个类, 且T中的静态方法被调用
3)T声明的一个静态字段被赋值
4)T声明的静态字段被使用

在这里,首次执行getInstance(),那么InstanceHolder会进行初始化。

任何的线程安全操作在底层都是对应指令重排序以及内存可见性的问题。操作系统才是根本啊~~

阅读 216

推荐阅读
刨刨代码的根
用户专栏

刨根问底拦不住~

0 人关注
8 篇文章
专栏主页