一、单例模式的安全问题
1. 传统
package thread;/*** 单例设计模式的安全问题* 常⻅的DCL(Double Check Lock)双端检查模式加了同步,但是在多线程下依然会 有线程安全问题。*/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) {//main线程操作System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());}}
对象只创建了一次,单线程没有问题
一般在单例模式下使用.getInstance()创建对象;
但并不是所有 有 私有构造方法,对外通过getInstance方法提供实例的情况就是单例模式。
注:
单例模式:一个类有且只有一个实例。
1,一个私有的构造器
2,一个私有的该类类型的变量
3,必须有一个共有的返回类型为该类类型的方法,用来返回这个唯一的变量
/baxianhua/p/9341953.html
2. 改为多线程操作测试
package thread;public class SingletonDemo2 {private static SingletonDemo2 instance = null;private SingletonDemo2() {System.out.println(Thread.currentThread().getName() +"\t SingletonDemo构造⽅法执⾏了");}public static SingletonDemo2 getInstance(){if (instance == null) {instance = new SingletonDemo2();}return instance;}public static void main(String[] args) {//多线程操作for (int i = 0; i < 10; i++) {new Thread(()->{SingletonDemo2.getInstance();}, String.valueOf(i)).start();}}}
多线程下,实例化多少次不一定
3. 调整后,采⽤常⻅的DCL(Double Check Lock)双端检查模式加了同步,但是在多线程下依然会 有线程安全问题。
package thread;public class SingletonDemo3 {private static SingletonDemo3 instance = null;private SingletonDemo3() {System.out.println(Thread.currentThread().getName() +"\t SingletonDemo构造⽅法执⾏了");}public static SingletonDemo3 getInstance(){if (instance == null) {// 加锁,只能有一个线程执行下面synchronized (SingletonDemo3.class){if (instance == null) {instance = new SingletonDemo3();}}}return instance;}public static void main(String[] args) {//多线程操作for (int i = 0; i < 10; i++) {new Thread(()->{SingletonDemo3.getInstance();},String.valueOf(i)).start();}}}
但是,当前程序有bug。
这个漏洞⽐较tricky,很难捕捉,但是是存在的。
instance=new SingletonDemo();可以⼤致分为三步:
instance = new SingletonDemo();public static thread.SingletonDemo getInstance();Code:0: getstatic #11// Field instance:Lthread/SingletonDemo; 3: ifnonnull 376: ldc #12 // class thread/SingletonDemo 8: dup 9: astore_010: monitorenter11: getstatic #11// Field instance:Lthread/SingletonDemo; 14: ifnonnull 2717: new #12 // class thread/SingletonDemo 步骤1 20: dup21: invokespecial #13 // Method "<init>":()V 步骤2 24: putstatic #11// Field instance:Lthread/SingletonDemo;步骤3
底层Java Native Interface中的C语⾔代码内容,开辟空间的步骤memory = allocate(); //步骤1.分配对象内存空间 instance(memory); //步骤2.初始化对象 instance = memory; //步骤3.设置instance指向刚分配的内存地址,此时instance != null
剖析:
在多线程的环境下,由于有指令重排序的存在,DCL(双端检锁)机制不⼀定线程安全,我们可以加⼊ volatile可以禁⽌指令重排。
解决方法,给变量增加volatile
原因在与某⼀个线程执⾏到第⼀次检测,读取到的instance不为null时,instance的引⽤对象可能没有完成初始化。
memory = allocate(); //步骤1. 分配对象内存空间instance(memory); //步骤2.初始化对象instance = memory; //步骤3.设置instance指向刚分配的内存地址,此时instance !=null
步骤2和步骤3不存在数据依赖关系,⽽且⽆论重排前还是重排后,程序的执⾏结果在单线程中并没有改变,因此这种重排优化是允许的。
memory = allocate(); //步骤1. 分配对象内存空间 instance = memory; //步骤3.设置instance指向刚分配的内存地址,此时instance != null,但是对象还没有初始化完成! instance(memory); //步骤2.初始化对象
但是指令重排只会保证串⾏语义的执⾏⼀致性(单线程),并不关⼼多线程的语义⼀致性。
所以,当⼀条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
public static SingletonDemo getInstance(){ if (instance == null) { synchronized (SingletonDemo.class){ if (instance == null) { instance = new SingletonDemo(); //多线程情况下,可能发⽣指令重排 }}} return instance;}
如果发⽣指定重排,那么,
1. 此时内存已经分配,那么 instance=memory 不为null。2. 碰巧,若遇到线程此时挂起,那么 instance(memory) 还未执⾏,对象还未初始化。3. 导致了 instance!=null ,所以两次判断都跳过,最后返回的 instance`没有任何内容,还没初始化。
解决的⽅法就是对 singletondemo 对象添加上 volatile 关键字,禁⽌指令重排。
private static volatile SingletonDemo singletonDemo=null;