懒汉式单例模式为什么需要进行二次判空

6次阅读

共计 2416 个字符,预计需要花费 7 分钟才能阅读完成。

前言:

在本文中会使用代码进行展示懒汉单例模式为什么需要进行二次判空;代码中使用到 CountDownLatch 倒计时器,不清楚CountDownLatch 使用的请参考此文 倒计时器:CountDownLatch。

代码:

1、懒汉式 单例模式:

public class Singleton {
    // 使用 volatile 禁止指令重排序
    private static volatile Singleton sin = null;

    public static int i = 0;// 标识有几个线程获取到了锁
    public static int j = 0;// 标识系统中到底生成了几个实例

    // 将构造器的修饰符设置为 "private",可以防止在外部进行 new 实例对象
    private Singleton() {};

    // 获取实例对象的方法,公共的方法。public static Singleton getInstance() {
        // 第一次判空。if (sin == null) {
            // 加锁
            synchronized (Singleton.class) {
                i++;
                // 第二次判空。if (sin == null) {sin = new Singleton();
                    j++;
                }
            }
        }
        return sin;
    }
}

2、多线程并发调用 单例模式的测试类:(注:此处会简单介绍下 CountDownLatch 的原理)

public class ThreadTest implements Runnable  {
 
    /**
     * 实例化一个倒计数器, 初始倒计数为 10; 其实内部是将 AQS 的同步状态变量 state 设置为了 10,
     * 说明此时有 10 个线程获取到了共享锁
     */
    static final CountDownLatch latch = new CountDownLatch(10);
    static final ThreadTest demo = new ThreadTest();
 
    @Override
    public void run() {
        try {
            // 实例对象生成
            Singleton.getInstance();
            // 输出当前线程的名称
            System.out.println(Thread.currentThread().getName());
        } catch (Exception e) {e.printStackTrace();
        }
        finally {
            // 计数器进行减一, 说明有一个线程已经成功释放了共享锁
            latch.countDown();}
    }
 
    public static void main(String[] args) throws InterruptedException {
        // 创建一个长度为 10 的定长线程池
        ExecutorService exec = Executors.newFixedThreadPool(10);
        for (int i=0; i<10; i++){
            // 提交任务给线程池去执行
            exec.submit(demo);
        }
 
        /**
         * 等待检查, 阻塞 main 主线程, 只有当 CountDownLatch 倒计数器为 0 时,
         * 也就是获得共享锁的线程全部释放了共享锁后, 才会唤醒阻塞的 main 主线程
         */
        latch.await();
 
        // 在开启的 10 个线程中几个线程获取到了锁
        System.out.println("共有 (" + Singleton.i + ") 个线程获取到对象锁");
        // 最终生成了几个 Singleton 实例
        System.out.println("最终生成了 (" + Singleton.j + ") 个 Singleton 实例对象");
 
        // 关闭线程池
        exec.shutdown();}
}

3、运行上面的 mian 方法,会得到以下的 一种 输出结果(存在多种输出结果):

pool-1-thread-1
pool-1-thread-7
pool-1-thread-5
pool-1-thread-3
pool-1-thread-6
pool-1-thread-2
pool-1-thread-4
pool-1-thread-9
pool-1-thread-8
pool-1-thread-10
共有 (2) 个线程获取到对象锁
最终生成了 (1) 个 Singleton 实例对象

总结:

​       从运行结果可以看出,如果不进行第二次判空的话,那么在竟锁池((锁池)中如果还有活跃的线程在等待获取的锁的话,在锁释放后就会再次竞争获取锁,获取的锁的线程进入 ” 就绪状态 ”,当 CPU 分配其 ” 时间片 ” 后进行线程的调度,从而线程进入 ” 运行中状态 ”,并会去执行同步的代码块,如果在没加如二次判空的话,就会导致系统中存在多个实例,而在进行判空后,即使你获取到了锁,但在执行同步代码块时也会直接跳过。

竟锁池((锁池)的概念 参考地址:Java 中的锁池和等待池

扩展:

懒汉式单例模式中 volatile 修饰符的作用:

代码中 private static volatile Singleton sin = null;   volatile 修饰符的作用是什么呢?

volatile 修饰变量只是为了禁止指令重排序,因为在 sin = new Singleton(); 创建对象时,底层会分为四个指令执行:(下面是正确的指令执行顺序)
①、如果类没有被加载过,则进行类的加载
②、在堆中开辟内存空间 adr,用于存放创建的对象
③、执行构造方法实例化对象
④、将堆中开辟的内存地址 adr 赋值给被 volatile 修饰的引用变量 sin

如果 sin 引用变量不使用 volatile 修饰的话,则可能由于编译器和处理器对指令进行了重排序,导致第④步在第③步之前执行,此时 sin 引用变量不为 null 了,但是 sin 这个引用变量所指向的堆中内存地址中的对象是还没被实例化的,实例对象还是 null 的;那么在第一次判空时就不为 null 了,然后去使用时就会报 NPE 空指针异常了。

❤不要忘记留下你学习的足迹 [点赞 + 收藏 + 评论]嘿嘿ヾ

一切看文章不点赞都是“耍流氓”,嘿嘿ヾ (◍°∇°◍)ノ゙!开个玩笑,动一动你的小手,点赞就完事了,你每个人出一份力量(点赞 + 评论) 就会让更多的学习者加入进来!非常感谢!~ω~=

正文完
 0