文章目录
1. 单例模式中的懒汉式
2. 单例模式中的双检索/双重校验锁(DCL)
3. 对象创建过程
4. 基于volatile解决方案
1. 单例模式中的懒汉式
1.1 线程不安全的懒汉式
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 是否是多线程安全:否
- 实现难度: 易
- 这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作
1.2 线程安全的懒汉式
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 是否是多线程安全: 是
- 实现难度: 易
- 这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
2. 单例模式中的双检索/双重校验锁(DCL)
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
- JDK 版本:JDK1.5 起
- 是否 Lazy 初始化:是
- 是否多线程安全:是
- 实现难度:较复杂
描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。
3. 对象创建过程
3.1 CPU指令层面可能分为三个步骤:(如下伪代码)
memory = allocate(); //1分配对象内存空间
ctorInstance(memory); // 2初始化对象
instance = memory; //3设置instance指向更分配的内存地址
这几个步骤的执行时序在单线程情况下可能如下:
![](https://i-blog.csdnimg.cn/blog_migrate/b69081182c0470b4e56fe4db01ec4319.png)
多线程情况下:
![](https://i-blog.csdnimg.cn/blog_migrate/6c7fefb22e7a79a4ac5b10f78efe1abf.png)
3.2 或者查看字节码层面上的对象初始化过程
public class JustTest {
public static void main(String[] args) {
Object o = new Object();
}
}
这里需要安装插件查看字节码:
此时 如果发生指令重排序
线程1半初始化的时候,线程2此时访问,由于半初始化的时候对象不为Null,所以线程2会直接拿已经半初始化的对象,而没有拿到完全初始化的对象。
4. 基于volatile解决方案
4.1 将变量singleton声明为volatile即可
public class Singleton {
// 通过volatile关键字来确保安全
private volatile static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
当 singleton 声明为 volatile后,步骤 2、3 就不会被重排序了
4.2 volatile如何解决指令重排序(实现细节)
-
编译器层面
-
JVM层面
JSR内存屏障
-
hotspot实现
bytecodeinterpreter.cpp
int field_offset = cache->f2_as_index(); if (cache->is_volatile()) { if (support_IRIW_for_not_multiple_copy_atomic_cpu) { OrderAccess::fence(); }
orderaccess_linux_x86.inline.hpp
inline void OrderAccess::fence() { if (os::is_MP()) { // always use locked addl since mfence is sometimes expensive #ifdef AMD64 __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory"); #else __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory"); #endif } }
“lock; addl $0,0(%%esp)” 相当于一个内存屏障(Memory Barrier, 指重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障,但如果有两个或者更多CPU访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性。addl $0,0(%%esp)是一条空操作,可以让前面volatile变量的修改对其它CPU立即可见