Volatile实现原理以及应用附加相关面试题

本文介绍了Java中volatile关键字的基本概述、如何保证一致性和有序性。volatile通过内存屏障保证了多线程环境下的可见性和有序性,防止指令重排序。此外,文章还讨论了volatile在解决单例模式的双重检测问题中的作用。
摘要由CSDN通过智能技术生成

基本概述

一致性

JVM内存的会分为主内存和线程内存,比如一个对象等数据就会放在主内存的堆里面,堆内存主要是物理内存,如果一个线程需要多次访问主内存中的一个对象,那么就会出现多次物理内存的查找,这样带来的性能损耗会比较大,为了解决这个问题,JVM会把线程使用频繁的对象缓存到线程之中,在缓存中高速访问,那么就会降低损耗。但是在多线程的环境下会出现幻读,或者重复读的问题,为了解决这个问题,Java推出了volatile关键字。

volatile是一个特殊的修饰符,只有成员变量才能使用它。在Java并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的。volatile变量可以保证下一个读取操作会在前一个写操作之后发生。线程都会直接从内存中读取该变量并且不缓存它。这就确保了线程读取到的变量是同内存中是一致的。

有序性

JVM在编译字节码的时候,就是把.java文件编译为.class文件的时候,为了将性能优化,有的时候会将指令进行重新排序。这在一些并发场景就会产生一些问题。就比如在双重判断的懒汉单例模式中,对instance判断是否存在那部分代码被两个线程访问,会导致一个线程拿到未完全实例化的一个对象。可以使用volatile来解决这个问题。

用volatile关键字修饰成员变量,可以确保被修饰的成员变量在JVM编译字节码的时候不被重排序。

实现原理

volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令前会加入读屏障

如何保证可见性

  • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
  • 读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

如何保证有序性

  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
  • 注意,这仅仅是在单线程角度上的有序性

解决单例模式的双重检测问题

public final class Singleton {
 private Singleton() { }
 private static Singleton INSTANCE = null;
 public static Singleton getInstance() { 
 if(INSTANCE == null) { // t2
 // 首次访问会同步,而之后的使用没有 synchronized
 synchronized(Singleton.class) {
 if (INSTANCE == null) { // t1
 INSTANCE = new Singleton();
 } 
 }
 }
 return INSTANCE;
 }
}

这是没有加volatile关键字的双重检测懒汉单例模式,这种单例模式可以保证线程安全,并且减小了锁的粒度,以及加锁的次数,首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁,但是问题在于第一个INSTANCE的判断,这个判断是在同步代码块之外的。在多线程环境下会出现问题。

0: getstatic #2 
3: ifnonnull 37
6: ldc #3 
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 
14: ifnonnull 27
17: new #3 
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 
40: areturn

这是getInstance对应的字节码

其中

  • 17 表示创建对象,将对象引用入栈 // new Singleton
  • 20 表示复制一份对象引用 // 引用地址
  • 21 表示利用一个对象引用,调用构造方法
  • 24 表示利用一个对象引用,赋值给 static INSTANCE

也许 jvm 会优化为:先执行 24,再执行 21。如果两个线程 t1,t2 按如下时间序列执行:

关键在于 0: getstatic 这行代码在 monitor 控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取 INSTANCE 变量的值

这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例

对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效 

public final class Singleton {
 private Singleton() { }
 private static volatile Singleton INSTANCE = null;
 public static Singleton getInstance() {
 // 实例没创建,才会进入内部的 synchronized代码块
 if (INSTANCE == null) { 
 synchronized (Singleton.class) { // t2
 // 也许有其它线程已经创建实例,所以再判断一次
 if (INSTANCE == null) { // t1
 INSTANCE = new Singleton();
 }
 }
 }
 return INSTANCE;
 }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值