一.示例代码:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {//检查判断1
synchronized (Singleton.class) {
if (instance == null) {//检查判断2
instance = new Singleton();
}
}
}
return instance;
}
}
- 思路:
1)当线程A、B同时调用getInstance()方法,他们同时发现 instance == null成立(检查判断1),同时去获取的Singleton.class锁
2)其中线程A获取到锁,线程B 处于等待状态;线程A会创建一个SingleTon实例,之后释放锁
3)线程A释放锁后,线程B 被唤醒,线程B获取到锁,然后线程B检查
instance == null 不成立(检查判断2),不会再创建Singleton实例对象
二.存在的问题:
上述单例模式的创建方式,线程安全、懒加载、性能高,但在new Singleton()的操作中却可能带来空指针的异常问题
- 我们认为的new Singleton()操作
- 1)分配内存地址 M
- 2)在内存 M 上初始化Singleton 对象
- 3)将M的地址赋值给 instance 对象
- JVM编译优化后可能的new Singleton()操作
- 1)分配内存地址 M
- 2)将M的地址赋值给instance变量
- 3)在内存M上初始化 Singleton 对象
- 异常发生过程(如上图,JVM创建new Instance()对象时先赋值再初始化)
- 1)线程A先执行getInstance()方法,当线程A在执行完变量的内存地址赋值(尚未初始化)时,发生线程切换,线程B获得CPU的执行权
- 2)线程B在执行第一个判断,发现 instance == null条件不成立,直接返回instance,但此时instance并没有初始化,此时访问instance对象的成员变量就可能发生空指针异常
三.解决方式:
上述问题出现的本质原因是(线程切换带来的原子性问题),JVM在编译时的执行重排序造成的,所以只要禁止指令重排序,就可以解决这个问题,所以需要在Singleton对象的成员变量instance前加volatile关键字
private volatile static Singleton instance;
四.扩展-线程切换:
- 线程切换:
操作系统允许某个进程执行一小段时间,如50ms,过了50ms操作系统会重新选择一个进程来执行(任务切换),这个50ms称为时间片
Java并发是基于多线程的,大多数的并发bug都是由于线程切换造成的
Java的一条语句对应的cpu指令可能是多条,其中任意一条cpu指令在执行完都可能发生线程切换
- 如:count += 1,对应cpu 指令:
- 1)将变量count从内存加载到cpu寄存器
- 2)寄存器中 +1
- 3)将结果写入内存(缓存机制写入的可能是cpu而不是内存)