美文网首页
并发线程-双重检查锁定问题

并发线程-双重检查锁定问题

作者: 一只狗被牵着走 | 来源:发表于2020-09-04 20:21 被阅读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());
        }
    }
}

相关文章

网友评论

      本文标题:并发线程-双重检查锁定问题

      本文链接:https://www.haomeiwen.com/subject/fyttektx.html