单例模式--双重检查判断--编译优化后异常--浅析

一.示例代码:

public class Singleton {

   private static Singleton instance;

   private Singleton() {}

   public static Singleton getInstance() {
       if (instance == null) {//检查判断1
           synchronized (Singleton.class) {
               if (instance == null) {//检查判断2
                   instance = new Singleton();
               }
           }
       }
       return instance;
   }
}
  • 思路:
    1)当线程A、B同时调用getInstance()方法,他们同时发现 instance == null成立(检查判断1),同时去获取的Singleton.class锁
    2)其中线程A获取到锁,线程B 处于等待状态;线程A会创建一个SingleTon实例,之后释放锁
    3)线程A释放锁后,线程B 被唤醒,线程B获取到锁,然后线程B检查
    instance == null 不成立(检查判断2),不会再创建Singleton实例对象

二.存在的问题:
上述单例模式的创建方式,线程安全、懒加载、性能高,但在new Singleton()的操作中却可能带来空指针的异常问题

  • 我们认为的new Singleton()操作
  • 1)分配内存地址 M
  • 2)在内存 M 上初始化Singleton 对象
  • 3)将M的地址赋值给 instance 对象
  • JVM编译优化后可能的new Singleton()操作
  • 1)分配内存地址 M
  • 2)将M的地址赋值给instance变量
  • 3)在内存M上初始化 Singleton 对象

在这里插入图片描述

  • 异常发生过程(如上图,JVM创建new Instance()对象时先赋值再初始化)
  • 1)线程A先执行getInstance()方法,当线程A在执行完变量的内存地址赋值(尚未初始化)时,发生线程切换,线程B获得CPU的执行权
  • 2)线程B在执行第一个判断,发现 instance == null条件不成立,直接返回instance,但此时instance并没有初始化,此时访问instance对象的成员变量就可能发生空指针异常

三.解决方式:
上述问题出现的本质原因是(线程切换带来的原子性问题),JVM在编译时的执行重排序造成的,所以只要禁止指令重排序,就可以解决这个问题,所以需要在Singleton对象的成员变量instance前加volatile关键字

   private volatile static Singleton instance;

四.扩展-线程切换:

  • 线程切换:

操作系统允许某个进程执行一小段时间,如50ms,过了50ms操作系统会重新选择一个进程来执行(任务切换),这个50ms称为时间片
Java并发是基于多线程的,大多数的并发bug都是由于线程切换造成的

Java的一条语句对应的cpu指令可能是多条,其中任意一条cpu指令在执行完都可能发生线程切换

  • 如:count += 1,对应cpu 指令:
  • 1)将变量count从内存加载到cpu寄存器
  • 2)寄存器中 +1
  • 3)将结果写入内存(缓存机制写入的可能是cpu而不是内存)

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值