目录
三、更改synchronized加锁位置,解决二中的效率问题
单例模式核心思想
构造器私有使得外界无法通过构造器实例化Singleton类,要取得实例只能通过getInstance()方法。
一、一般的单例模式
class Singleton{
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){ // 代码A:判断检查
singleton = new Singleton(); // 代码B:创建实例
}
return singleton;
}
}
问题
- 该方法在单线程下能够正常运行,但是在多线程环境下会出现由于没有同步措施而导致产生多个单例对象的情况。
原因分析
多线程中,当有两个线程1和2同时执行到 if 条件判断语句【代码A处】,线程1判断singleton为空,在准备执行new对象时【代码B】 时让出了CPU时间片,此时由于线程1未执行new对象操作,线程2也判断singleton为空,接着执行 执行new对象时【代码B】,此时创建了一个实例对象;A获取了CPU时间片后接着执行执行new对象时【代码B】,也创建了实例对象,这就导致多个单例对象的情况
二、加入synchronized关键字,解决一中的同步问题
class Singleton{
private static Singleton singleton;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(singleton == null){
singleton = new Singleton(); //代码B:创建实例
}
return singleton;
}
}
通过synchronized关键字可解决多线程并发的问题,但是却带来了效率问题:
我们的目的是只创建一个实例,即【代码B】处代码只会执行一次,也正是这个地方才需要同步,后面创建了实例之后,singleton非空就会直接返回对象引用,而不用每次都在同步代码块中进行非空验证。
三、更改synchronized加锁位置,解决二中的效率问题
class Singleton{
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
synchronized(Singleton.class){
singleton = new Singleton(); //代码B:创建实例
}
}
return singleton;
}
}
问题
- 存在和【一般单例模式】中同样的问题,即多个线程同时执行到条件判断语句时,会创建多个实例。
原因
- 同【一般单例模式】的原因一样
四、引入二次检查,解决三中的同步问题
class Singleton{
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){ //代码A:初次校验
synchronized(Singleton.class){ //代码B:加锁同步
if(singleton == null){ //代码C:二次校验
singleton = new Singleton(); //代码D:创建实例
}
}
}
return singleton;
}
}
流程
- 当线程1和线程2同时通过了初次校验【代码A】时,会通过synchronized关键字,根据获取锁的先后顺序来进行之后的逻辑。
- 假设线程1优先通过【代码B】拿到了锁,线程2则阻塞等待。
- 线程1执行【代码C】和【代码D】,生成对象后,释放锁。
- 此时线程2获取锁,执行二次校验【代码C】,发现此时singleton已经不为空,固不再执行【代码D】
思考
或许有同学会有疑问,这种结构下,如果把最外层的初次校验【代码A】给去掉,也能实现当前效果。
解答
- 把最外层的初次校验【代码A】给去掉后,其代码结构就和【二】中的代码逻辑一样了,相当于对整个方法都进行了加锁。
- 不管是否当前对象为空,收到new对象的请求后,都会去做加锁、释放锁的操作。
- 固初次校验【代码A】的作用就是,在单例对象不为空时,直接返回单例对象
五、引入volatile关键字
到这里已经很完美了,看起来没有问题。No!!!因为这种双重检测机制在JDK1.5之前是有问题的,问题还是出在(//创建实例),由【指令重排机制】引起的。
问题
一般来讲,当实例化一个对象的时候,会经历(这个记录一下我个人的理解:初始化指的是类,加在家class文件,解析class文件;实例化指的是对象,即以下三步:)
//a. 内存分配
memory = allocate();
//b. 执行实例化对象的初始化
ctorInstance(memory);
//c. 返回引用:将内存空间的地址赋值给对应的引用,并返回
instance = memory;
这种方式产生的对象是一个完整的对象,可以正常使用。但是Java的【指令重排机制】可能会造成顺序的颠倒,即
a. 内存分配
b. 返回对象引用
c. 执行实例化对象的初始化
分析
- 线程1获取synchronized锁后,执行new Singleton()实例化对象
- 在new对象时,发生了【指令重排】,实际执行的代码逻辑为
- a. 内存分配:划分内存空间
- b. 返回对象引用:将引用该内存地址对应的引用返回
- 在还没有执行步骤c时,又来了一个线程2执行到了初始校验【代码A】处
- 因为此时线程1已经将引用返回,固线程2没有通过初始校验【代码A】,直接返回线程1中的引用
- 但是此时的引用,指向的内存地址的对象为空,就会抛出异常
关于volatile关键字的使用,可参考文章:知识点:多线程Volatile关键字_叫我吕小布-CSDN博客volatile关键字解释:保证可见性和有序性https://blog.csdn.net/qq_36769100/article/details/122961480
六、最终volatile + 双重检测机制的单例模式
public class Singleton{
private volatile static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}