必须加volatile
**
* 双重校验锁 DCL(DoubleCheckLock)
* 必须加volatile 防止指令重排
* 多线程情况下安全
*/
public class DCLType {
// 增加volatile修饰禁止指令重排
private static volatile DCLType INSTANCE;
private DCLType() {
}
//可以整个方法加锁 但此时锁粒度太粗了,所以可降低锁粒度
public static DCLType getInstance() {
//进行了判断,如果有一个完成了new,就不用参与锁竞争,效率提高
if (INSTANCE == null) {
//如果等于空 进行上锁,上完锁再进行new 完了后释放锁
//双重校验
synchronized (DCLType.class) {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new DCLType();
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(DCLType.getInstance().hashCode());
}).start();
}
}
}
原因:1.程序是存在乱序执行的 2.new一个对象的时候是存在半初始化状态的
假设thread 1 进行上锁,new申请内存空间,astore_1 和 invokespecial T.<init>这两步进行了指令重排,此时第二个线程进来了,此时thread 1 new了对象,进行INSTANCE==null判断,不为空,不需要上锁,直接拿到对象进行使用。此时INSTANCE处于半初始状态,第二个线程就使用了半初始化状态的对象。(虽然这种情况基本不存在,但还是要加volatile)volatile修饰的内存空间上面执行指令,会通过内存屏障完成禁止指令重排。最终的实现通过lock addl实现,锁总线完成volatile的两个功能,所有的CPU都这样锁。
内存屏障:
JVM(规范)级别的屏障:
LoadLoad屏障 (读):对于这样的语句 Load1;LoadLoad;Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障(写):Store1;StoreStore;Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其他处理器可见
LoadStore屏障 :同理,在后续写入操作被刷出前,保证前面要读取的数据被读取完毕
StoreLoad屏障 :同理,在后续读取操作执行前,保证之前写入对所有处理器可见。
jvm规定了8种happens-before原则,这八种之外的可重排序;
as-if-serial:看上去像有序执行,不管如何重排序,单线程执行结果不会改变;
硬件级别屏障