volatile、双重校验锁实现对象单例(线程安全)

预备知识:volatile关键字主要作用是保证变量的内存可见性和禁止指令重排序。JMM(Java Memory Model)是 Java 虚拟机规范中所定义的一种内存模型,JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的工作内存(或者叫本地内存),工作内存中存储了被该线程使用过的共享变量的副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。
在这里插入图片描述
然而,JMM 这样的规定可能会导致线程对共享变量的修改没有即时更新到主内存,或者线程没能够即时将共享变量的最新值同步到工作内存中,从而使得线程在使用共享变量的值时,该值并不是最新的。因此出现了内存可见性的问题。
内存可见性是指当一个线程修改了某个变量的值,其它线程总是能知道这个变量变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程 B 在使用 V 的值时,能立即读到 V 的最新值。多线程并发时实现内存可见性有两种方式:加锁或者使用volatile关键字。
为什么加锁后就保证了变量的内存可见性了? 因为当一个线程进入 synchronized 代码块后,线程获取到锁,会清空本地内存,然后从主内存中拷贝共享变量的最新值到本地内存作为副本,执行代码,又将修改后的副本值刷新到主内存中,最后线程释放锁。
而使用 volatile 修饰共享变量后,每个线程要操作变量时会拷贝主内存中的变量到本地内存作为副本,而当线程操作本地变量副本写回主内存后,会通过CPU总线嗅探机制来通知其他线程该变量副本已经失效,需要从主内存中重新读取。
注意volatile并不保证数据操作的原子性,因为虽然每个线程从主内存中读取到的变量副本都是最新的,但是每个线程对各自的变量副本进行的操作可能不是原子操作。比如有个volatile i,2个线程要进行i++操作,而i++过程可以分为读取i的值,对这个值加1,再写回缓存中。线程A从主内存中读取i的值为100,还没来得及操作,线程B也读取i的值100,对100+1得到101,写回工作内存,从工作内存刷入主内存,通知线程A,但此时A的读取这个原子操作已经结束了,这个通知来晚了,线程A继续把100+1得到101,写入工作内存,又刷入主内存,此时虽然两个线程都进行了+1操作,但100实际上只增加了1变成101,可见volatile不能保证原子性。

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

进行了2次null检验

  1. 第一次检验是在synchronized同步块外,这是因为单例模式只会创建一个实例,并通过 getUniqueInstance 方法返回 Singleton 对象,所以如果已经创建了 Singleton 对象,就不用进入同步代码块,不用竞争锁,直接返回前面创建的实例即可。
  2. 第二次检验是为了保证同步,加入线程A通过第一次判断进入同步代码块,获得了锁,但尚未执行,此时线程B也通过了第一次判断阻塞在同步块,等到线程A new了一个Singleton实例后,释放锁,线程B此时获取到锁进入同步块,但检验发现Singleton实例已经创建了,因此线程B进入同步块什么也不做。如果要是不再次检验null,线程B就又会重新new一个Singleton实例覆盖调线程A new的实例了。

注意uniqueInstance 采用 volatile 关键字修饰也是很有必要的,uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址

由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance()发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值