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,必须有一个共有的返回类型为该类类型的方法,用来返回这个唯一的变量
https://www.cnblogs.com/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 37
6: ldc #12 // class thread/SingletonDemo
8: dup
9: astore_0
10: monitorenter
11: getstatic #11 // Field instance:Lthread/SingletonDemo;
14: ifnonnull 27
17: new #12 // class thread/SingletonDemo 步骤1
20: dup
21: invokespecial #13 // Method "":()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;