并发线程-双重检查锁定问题
2020-09-04 本文已影响0人
一只狗被牵着走
双重检查锁定问题:Double-checked Locking
1、先来看问题代码
有线程安全问题的代码块-双重检查锁定问题- 代码
// 单例模式构建对象
public static LocalCache getInstance(){
// 双重检测锁,提高运行效率
if (instance == null){
synchronized (LocalCache.class){
if (instance == null) {
instance = new LocalCache();
}
}
}
return instance;
}
代码(该方法)的预期目标是使用 懒汉模式 的单例设计模式获取对象,阿里插件显示这段代码是线程不安全的
2、改进方案
2.1、懒汉模式改为饿汉模式
懒汉模式的一种实现方式这种思路有几种实现方式,这里贴一种,是静态代码块实例化一次对象
- 代码
static {
instance = new LocalCache();
}
// 单例模式构建对象
public static LocalCache getInstance(){
return instance;
}
2.2、同为懒汉模式下的代码改进
可行的方案
instance变量加上 volatile 关键字,保证变量值的可见性
/**
* volatile的可见性,可以确保拿到 instance 的最终值
*/
private static volatile LocalCache instance;
// 单例模式构建对象
public static LocalCache getInstance(){
// 双重检测锁,提高运行效率
if (instance == null){
synchronized (LocalCache.class){
if (instance == null) {
instance = new LocalCache();
}
}
}
return instance;
}
否定的方案(这个方案也不能保证线程安全,这里有点不太懂)
// 单例模式构建对象
public static LocalCache getInstance(){
// 双重检测锁,提高运行效率
if (instance == null){
synchronized (LocalCache.class){
if (instance == null) {
LocalCache localCache = new LocalCache();
instance = localCache;
}
}
}
return instance;
}
3、双重检测锁定问题产生的原因
3.1、简单来说
instance = new LocalCache();
这行代码有三步操作(《码出高效Java开发手册》P233页提到有两步操作,个人感觉不太好解释):
- 初始化 LocalCache 实例
- 为本来为null的instance变量开辟内存空间,并确定默认大小(这一点《码出高效》P233页书中并没有提到)
- 将对象地址写进 instance 字段
这三步操作并不是原子化的
3.2、举个例子
- 线程A进入到
if(instance == null){
的时候,instance为null - 线程A进入同步代码块(synchronized括起来的代码块),到
instance = new LocalCache();
时执行了 为instance开辟内存空间 和 将对象的引用存入内存空间 的动作,但是没有实例化LocalCache对象 - 线程B执行到
if(instance == null){
的时候,instance不为null
(但是实际没有指向某个堆内的内存,简而言之,这块内存空间(栈的内存空间)的引用地址指向的(堆的)内存空间中没有实际对象),所以直接return了一个中间态(我自己起的名字。。)的instance - 线程B中,接下来的代码逻辑中,拿到instance的值其实是有问题的(有啥问题?-TODO- 反正是有问题的-_-)
这篇blog有相关介绍
指令重排/指令优化 导致的线程安全问题
所以这里涉及到指令重排
的问题(可能也有叫“指令优化”的-《码出高效》P232-P232有提到),即#3.1的三步操作,CPU在执行的时候并不会根据代码里理解的顺序(从上到下、从左到右)执行,会判断怎样的组合可以提高效率,重新排列指令执行的顺序(如图)
3.3、使用了volatile之后
这里用到的是volatile的防止指令重排的能力(JDK1.5之后才有的)-- volatile还有一个
可见性
的能力,这里貌似没有体现(下篇文章探讨volatile 可见性/指令重排 问题)
- 线程A进入到
if(instance == null){
的时候,instance为null - 线程A进入同步代码块(synchronized括起来的代码块),到
instance = new LocalCache();
时执行了 为instance开辟内存空间 和 实例化LocalCache对象 的动作,但是没有将对象的引用存入内存空间 - 线程B执行到
if(instance == null){
的时候,instance为null
- 接下去就是预期的执行流程了
4、复现双重检测问题的方式-供参考
!!实际复现过程中并没有复现问题,严重怀疑是复现方式还可以改进,以下复现方式仅供参考
懒汉模式加载的单例对象类
public class LocalCache {
private static LocalCache instance;
// 构造方法私有化,防止实例化
private LocalCache() {}
// 单例模式构建对象
public static LocalCache getInstance() throws InterruptedException {
// // 双重检测锁,提高运行效率
if (instance == null){
synchronized (LocalCache.class){
if (instance == null) {
instance = new LocalCache();
}
}
}
return instance;
}
}
建了两个线程工厂,每个工厂里面有两根线程(总共4根),模拟多线程环境(有更简便的写法)
public class TestJava {
public static void main(String[] args) {
BlockingQueue blockingDeque = new LinkedBlockingDeque(2);
TestThreadFactory firstFactory = new TestThreadFactory("第一个线程池");
TestThreadFactory secondFactory = new TestThreadFactory("第二个线程池");
TestRejectHandler testRejectHandler = new TestRejectHandler();
ThreadPoolExecutor firstThreadPool = new ThreadPoolExecutor(2, 2, Integer.MAX_VALUE, TimeUnit.SECONDS, blockingDeque, firstFactory, testRejectHandler);
ThreadPoolExecutor secondThreadPool = new ThreadPoolExecutor(2, 2, Integer.MAX_VALUE, TimeUnit.SECONDS,
blockingDeque, secondFactory, testRejectHandler);
Task task = new Task();
for (int i = 0; i < 2; i++){
firstThreadPool.execute(task);
secondThreadPool.execute(task);
}
}
/**
* 线程工厂
*/
public static class TestThreadFactory implements ThreadFactory{
private final String namePrefix;
private final AtomicInteger nextId = new AtomicInteger(1);
public TestThreadFactory(String namePrefix) {
this.namePrefix = "TestThreadFactory's " + namePrefix + "-worker-";
}
@Override
public Thread newThread(Runnable task) {
String name = namePrefix + nextId.getAndIncrement();
Thread thread = new Thread(null, task, name, 0);
System.out.println(thread);
return thread;
}
}
/**
* 实际执行任务
*/
public static class Task implements Runnable{
private final AtomicLong count = new AtomicLong(0L);
@Override
public void run() {
try {
LocalCache instance = LocalCache.getInstance();
// todo 这里做一些instance对象的操作
System.out.println("running_" + count.getAndIncrement() + ", instance: " + instance);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 当线程异常的时候,可以打印线程异常堆栈
*/
public static class TestRejectHandler implements RejectedExecutionHandler{
@Override
public void rejectedExecution(Runnable task, ThreadPoolExecutor executor) {
System.out.println("task rejected. " + executor.toString());
}
}
}