单例模式中的双重检查锁
DCL双重检查锁
DCL全称为Double Check Lock,中文为双重检查锁,
DCL的简单实现
/**
* DLC单例模式的双重检查锁
* @author hanhuafeng
* @version V1.0
* @description
* @date 2021/1/9
*/
public class DlcDoubleCheckLock {
private static volatile DlcDoubleCheckLock dlcDoubleCheckLock = null;
/**
* 必须要重新实现一个无参构造方法,因为这个bean是需要我们自己代码实例化,而不能让用户实例化
*/
private DlcDoubleCheckLock() {
}
public static DlcDoubleCheckLock getInstance() {
if (dlcDoubleCheckLock == null) {
synchronized (DlcDoubleCheckLock.class) {
if (dlcDoubleCheckLock == null) {
dlcDoubleCheckLock = new DlcDoubleCheckLock();
}
}
}
return dlcDoubleCheckLock;
}
}
volatile
保证线程可见性(线程之间的变量可被重读到)
- MESI
- 缓存一致性协议
禁止指令重排序(CPU)
-
应用场景——DCL单例
-
双重检查锁
-
是否需要增加volatile?
需要增加,因为可能会有指令重排序的问题
-
-
volatile的缺点
无法保证原子性,即不能替代synchronized
可从如下代码中看出来
/**
* 测试Volatile不具备原子性,无法替代synchronized,只能保证线程的可见性
* @author hanhuafeng
* @version V1.0
* @description
* @date 2021/1/9
*/
public class VolatileTest {
volatile int count = 0;
void m() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
public static void main(String[] args) {
VolatileTest volatileTest = new VolatileTest();
List<Thread> threads = new LinkedList<>();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(volatileTest::m, "thread-" + i));
}
threads.forEach(Thread::start);
threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(volatileTest.count);
// 可能是26421
// 可能是24113
// 也可能是其他各类答案
}
}
原因分析:
首先代码中如果不加入volatile,那么各个线程中,对于count这个变量肯定是不知道的,答案肯定是不正确的,如果加入了volatile,以3个线程为例,第一个线程++完成后为1,丢回了内存中,线程2,3都能拿到加完后的值1,在自身线程中++后又丢回内存中,可以发现,此时count为2,而不是期盼的3,所以可以说Volatile不具备原子性,无法替代synchronized。
如何解决这个问题?
只需要加入synchronized即可,代码如下
/**
* 测试Volatile不具备原子性,无法替代synchronized,只能保证线程的可见性
* @author hanhuafeng
* @version V1.0
* @description
* @date 2021/1/9
*/
public class VolatileTest {
volatile int count = 0;
synchronized void m() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
public static void main(String[] args) {
VolatileTest volatileTest = new VolatileTest();
List<Thread> threads = new LinkedList<>();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(volatileTest::m, "thread-" + i));
}
threads.forEach(Thread::start);
threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(volatileTest.count);
// 可能是26421
// 可能是24113
// 也可能是其他各类答案
}
}
synchronized
关于synchronized可以看我的这篇文章 多线程与高并发——synchronized
DCL在面试中可能被问到的问题:
volatile是否需要加?
答:需要加,在计算机指令中,, CPU和编译器为了提升程序的执行效率, 通常会按照一定的规则对指令进行优化, 如果两条指令互不依赖, 有可能它们执行的顺序并不是源代码编写的顺序,这可以称之为指令重排序,比如正常情况下 instance = new Instance()可以分成三步:
分配对象内存空间(此时值为默认值)
初始化对象成员变量
设置instance(栈内存中)指向刚刚分配的内存地址, 此时instance != null (重点)
因为2和3两步不存在数据上的依赖关系, 即在单线程的情况下, 无论2和3谁先执行, 都不影响最终的结果, 所以在程序编译时, 有可能它的顺序就变成了
分配对象内存空间(此时值为默认值)
设置instance(栈内存中)指向刚刚分配的内存地址, 此时instance != null (重点)
初始化对象成员变量
假设此时第一个线程正好被指令重排序,且执行到了第二步——设置instance(栈内存中)指向刚刚分配的内存地址,此时的instance != null,那么第二个线程取的时候,他所拿到的,就是刚刚分配了默认值的对象,而不是我最终想要的结果。
那么加了volatile之后会有什么不同呢? volatile有三个特点:
- 保证可见性
- 不保证原子性
- 禁止指令重排