前言:
在本文中会使用代码进行展示懒汉单例模式为什么需要进行二次判空;代码中使用到 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-1pool-1-thread-7pool-1-thread-5pool-1-thread-3pool-1-thread-6pool-1-thread-2pool-1-thread-4pool-1-thread-9pool-1-thread-8pool-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空指针异常了。
❤不要忘记留下你学习的足迹 [点赞 + 收藏 + 评论]嘿嘿ヾ
一切看文章不点赞都是“耍流氓”,嘿嘿ヾ(◍°∇°◍)ノ゙!开个玩笑,动一动你的小手,点赞就完事了,你每个人出一份力量(点赞 + 评论)就会让更多的学习者加入进来!非常感谢! ̄ ̄=