volatile关键字浅析

volatile关键字

  • 定义:轻量级、硬件级别锁,保证了多线程的可见性和有序性,但无法保证原子性
  • 作用范围: 类的成员变量、类的静态成员变量
  • 可见性(所有线程可见)

1)volatile 变量,JVM 保证了每次读变量都从主内存中读变量本身,跳过复制到工作内存这一步
2)非volatile 变量,每次读变量都从工作内存中读变量的副本

  • 有序性(禁止指令重排序优化)

观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成
2)它会强制将对缓存的修改操作立即写入主存
3)如果是写操作,它会导致其他CPU中对应的缓存行无效

  • 内存屏障与volatile的关系
  • 什么是内存屏障
    1)确保一些特定操作执行的顺序
    2)影响一些数据的可见性(可能是某些指令执行后的结果)。编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。
    例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个CPU核心或者哪颗CPU执行的。
  • 关系
    1)volatile修饰的变量,Java内存模型将在写操作后插入一个写屏障指令,在读操作之前插入一个读屏障指令
  • 无法保证原子性的原因

如:volatile a++, 在JVM进行操作时会被分解为如下4步

mov    0xc(%r10),%r8d ; Load  
inc    %r8d           ; Increment
mov    %r8d,0xc(%r10) ; Store
lock addl $0x0,(%rsp) ; Write  #lock增加内存屏障

1)Load:读取volatile变量的值到local
2)Increment:执行自增操作
3)Store:存储将工作内存中修改后的值传递到主内存中
4)Write:将store的值写到主内存的变量中(注意增加了 lock前缀,形成了内存屏障)

原因:从Load到Store到Write,一共4步,其中最后一步的 lock前缀形成 内存屏障,JVM让这个最新的变量的值立刻刷新至主内存中,所有线程可见,即最后一步让所有的CPU内核获得了最新的值,但中间的几步从 Load到Store 是不安全的(如果其它的CPU在期间修改了值将会丢失)
volatile关键字浅析

  • 示例1:测试volatile修饰的变量自增没有原子性
   private static volatile long _longVal = 0;
   private static class LoopVolatile implements Runnable {
       @Override
       public void run() {
           method();
       }
   }
   private static class LoopVolatile2 implements Runnable {
       @Override
       public void run() {
           method();
       }
   }
   public static void method(){
       long val = 0;
       while (val < 10000000L) {
           _longVal++;
           val++;
       }
   }
   public static void main(String[] args){
       Thread t1 = new Thread(new LoopVolatile());
       t1.start();
       Thread t2 = new Thread(new LoopVolatile2());
       t2.start();
       while (t1.isAlive() || t2.isAlive()) {
       }
       System.out.println("final val is: " + _longVal);
   }

示例1 执行结果:
final val is: 13799600

  • 示例2:测试synchronized修饰的同步方法使,使变量自增保持原子性
   private static long _longVal = 0;
   private static class LoopVolatile implements Runnable {
       @Override
       public void run() {
           method();
       }
   }
   private static class LoopVolatile2 implements Runnable {
       @Override
       public void run() {
           method();
       }
   }
   public synchronized static void method(){
       long val = 0;
       while (val < 10000000L) {
           _longVal++;
           val++;
       }
   }
   public static void main(String[] args){
       Thread t1 = new Thread(new LoopVolatile());
       t1.start();
       Thread t2 = new Thread(new LoopVolatile2());
       t2.start();
       while (t1.isAlive() || t2.isAlive()) {
       }
       System.out.println("final val is: " + _longVal);
   }

示例2 执行结果:
final val is: 20000000

示例结果分析:我们期望的结果应该是示例2中执行的结果20000000,但示例1中volatile修饰的变量最终的结果却是13799600远小于期望值,结果和我们预期的不一致。
volatile关键字浅析
结合JVM内存结构的理解:
在JVM的内存结构中有一块区域为JVM虚拟机栈,每一个线程在运行时都有一个线程栈,线程栈中保存了线程运行时的变量信息。当线程访问某一个对象的值,先通过栈中对象的引用地址读取(read):对应的在堆中变量的值,然后把堆内存中变量的值载入(load)到线程的线程栈内存中,建立一个变量副本,在write操作之前,线程就不再和对象在堆内存中的变量值有交互,而是直接修改副本变量的值(use 和 assign可以多次操作),在修改完之后的某个时刻(线程退出前),将线程变量副本的值写回到对象在堆中的变量(store 和 write),这样堆内存中的变量的值就产生了变化。

但是这些操作read、load、use、assign并不是原子操作(单个操作是原子性,组合之后非原子),即在read和load之后,如果主内存中的变量已经被修改,但线程在工作内存中的变量由于已经被加载,不会产生变化。

如上文中的示例:
线程1在read和load后在工作内存中建立的 _longVal 变量副本的值为100,
线程2在read 和load后在工作内存中建立的 _longVal 变量副本的值为100,
线程1在进行 _longVal++操作后,将修改后的值写入到主内存中,主内存中的值设置为101,但线程2已经load的值为100,线程2在进行一系列操作后,又会将主内存的值改回101,这样就导致了示例1出现了并发数据不一致的问题。