对volatile的理解--从JMM以及单例模式剖析 (3)

当普通单例模式在多线程情况下:

/** * 普通单例模式 * */ public class SingletonDemo { private static SingletonDemo instance = null; private SingletonDemo() { System.out.println(Thread.currentThread().getName() + "\t 构造方法 SingletonDemo()"); } public static SingletonDemo getInstance() { if (instance == null) { instance = new SingletonDemo(); } return instance; } public static void main(String[] args) { //构造方法只会被执行一次 // System.out.println(getInstance() == getInstance()); // System.out.println(getInstance() == getInstance()); // System.out.println(getInstance() == getInstance()); //并发多线程后,构造方法会在一些情况下执行多次 for (int i = 0; i < 10; i++) { new Thread(() -> { SingletonDemo.getInstance(); }, "Thread " + i).start(); } } }

此时会出现两个线程运行了SingletonDemo的构造方法

image-20210705133847185

此时就违反了单例模式的规定,其构造方法在一些情况下会被执行多次

解决方式:

单例模式DCL代码

DCL (Double Check Lock双端检锁机制)在加锁前和加锁后都进行一次判断

public static SingletonDemo getInstance() { if (instance == null) { synchronized (SingletonDemo.class) { if (instance == null) { instance = new SingletonDemo(); } } } return instance; }

不仅两次判空让程序执行更有效率,同时对代码块加锁,保证了线程的安全性

但是!还存在问题!

什么问题?

大部分运行结果构造方法只会被执行一次,但指令重排机制会让程序很小的几率出现构造方法被执行多次

DCL(双端检锁)机制不一定线程安全,原因时有指令重排的存在,加入volatile可以禁止指令重排

原因是在某一个线程执行到第一次检测,读取到instance不为null时,instance的引用对象可能没有完成初始化。instance=new SingleDemo();可以被分为一下三步(伪代码):

memory = allocate();//1.分配对象内存空间 instance(memory); //2.初始化对象 instance = memory; //3.设置instance执行刚分配的内存地址,此时instance!=null

步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化时允许的

所以如果3步骤提前于步骤2,但是instance还没有初始化完成指令重排只会保证串行语义的执行的一致性(单线程),但并不关心多线程间的语义一致性。

所以当一条线程访问instance不为null时,由于instance示例未必已初始化完成,也就造成了线程安全问题。

此时加上volatile后就不会出现线程安全问题

private static volatile SingletonDemo instance = null;

因为volatile禁止了指令重排序的问题

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzdyss.html