原文地址https://www.hcyhj.cn/2018/11/21/delay-loadjava
最近开始看《java并发编程的艺术》一书,从里面get到了好些知识上的盲点,下面就延迟加载这个问题来分析一波~~编程
public class DelayLoad { private DelayLoad() { } private static DelayLoad instance; public static DelayLoad getInstance() { if (instance == null) { //步骤1 instance = new DelayLoad(); //步骤2 } return instance; } }
从上面的代码片断里,很容易发如今多线程并发状况下去调用getInstance是会出问题的.当A线程和B线程同时进入到步骤1处,便会实例化两个对象出来,A和B访问到的对象就不会是同一个。多线程
public class DelayLoad { private DelayLoad() { } private static DelayLoad instance; public static synchronized DelayLoad getInstance() { if (instance == null) { instance = new DelayLoad(); } return instance; } }
代码改为这样后,能够彻底保证并发状况下获取的instance实例都会是同一个,可是多个线程同时调用synchronized 修饰的方法,会有获取锁以及释放锁操做,这里会形成大量的性能损耗,得不偿失!并发
public class DelayLoad { private DelayLoad() { } private static DelayLoad instance; public static DelayLoad getInstance() { if (instance == null) { //第一次检查 synchronized (DelayLoad.class){ if (instance == null) { //第二次检查 instance = new DelayLoad(); //建立实例 } } } return instance; } }
我们这里用双重检测的方法来实现这个单例懒加载,用这种策略看上去貌似没有什么问题,多线程并发的状况下每每也就是在第一次检查时都会直接返回实例,这样就不会形成性能损耗.可是,这里有可能出现instance不一致的问题。对于这个问题咱们得先了解对象的初始化过程。函数
1.在堆上为DelayLoad对象分配足够大的空间,全部属性和方法都被设置成缺省值(数字为0,字符为null,布尔为false,而全部引用被设置成null)。
2.执行构造函数检查是否有父类,若是有父类会先调用父类的构造函数,这里假设DelayLoad没有父类,执行缺省值字段的赋值即方法的初始化动做。
3.执行构造函数.性能
上面建立实例的那一步在cpu上可能通过以下操做:线程
memory = allocate(); //1分配对象内存空间 initInstance(memory);//2初始化对象 instance = memory; //3设置instance指向刚分配的内存地址
但实际上执行的过程当中,2和3步骤有可能进行指令重排,也就是按132的顺序执行,这样就会致使instance指向的是一个属性和值都是缺省值的对象。而后被一个竞争线程所拿到并进行使用。
code
public class DelayLoad { private DelayLoad() { } private static volatile DelayLoad instance; public static DelayLoad getInstance() { if (instance == null) { synchronized (DelayLoad.class){ if (instance == null) { instance = new DelayLoad(); } } } return instance; } }
代码改为上述状况后,在设置instance指向更分配的内存地址以前会有StoreStore内存屏障,执行代码会禁止指令重排,这样我们拿到的instance都是通过初始化过的。对象
public class DelayLoad { private DelayLoad() { } private static class DelayLoadHolder { public static DelayLoad instance = new DelayLoad(); } public static DelayLoad getInstance() { return DelayLoadHolder .instance; } }
该延迟加载方案是基于JVM的类初始化原理实现的。在执行类的初始化期间,JVM会去获取一个锁,该锁能够同步多个线程对同一个类的初始化。类只会被加载一次,在加载完成以前对其余线程都是不可见的。这样也能保证获取到的instance也是同一个。blog