【Java多线程】volatile关键字解析(五)

一、volatile是什么

  volatile在java语言中是一个关键字,用于修饰变量。被volatile修饰的变量后,表示这个变量在不同线程中是共享,编译器与运行时都会注意到这个变量是共享的,因此不会对该变量进行重排序。

volatile关键字的两层语义

  一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,volatile变量具备以下特性:

  1. 具备可见性。一个线程对一个volatile变量的写对其他线程立马可见。

  2. 禁止一定的指令重排序。
  3. volatile变量并不保证原子性。对任意单个volatile变量的读/写具备原子性,但类似于i++的复合操作不具有原子性。

语义1、可见性变量的共享

  示例

 1 public class TestVolatile {
 2 
 3     static boolean found = false;
 4 
 5     public static void main(String[] args) {
 6 
 7         new Thread(new Runnable() {
 8             public void run() {
 9                 System.out.println(Thread.currentThread().getName() + ":等基友送笔来...");
10 
11                 while (!found) {
12                 }
13 
14                 System.out.println(Thread.currentThread().getName() + ":笔来了,开始写字...");
15             }
16         }, "我的线程").start();
17 
18         new Thread(new Runnable() {
19             public void run() {
20                 try {
21                     Thread.sleep(2000);
22                 } catch (InterruptedException e) {
23                     e.printStackTrace();
24                 }
25 
26                 System.out.println(Thread.currentThread().getName() + ":基友找到笔了,送过去...");
27                 found = true;
28             }
29         }, "基友线程").start();
30     }
31 }  

  上面的代码是一种典型用法,检查某个标记(found)的状态判断是否退出循环。但是上面的代码有可能会结束,也可能永远不会结束。因为每一个线程都拥有自己的工作内存,当一个线程读取变量的时候,会把变量在自己内存中拷贝一份。之后访问该变量的时候都通过访问线程的工作内存,如果修改该变量,则将工作内存中的变量修改,然后再更新到主存上。这种机制让程序可以更快的运行,然而也会遇到像上述例子这样的情况。

  存在一种情况,found变量被分别拷贝到我的线程、基友线程两个线程中,此时found为false。基友线程开始循环,我的线程修改本地found变量称为true,并将found=true回写到主存,但是found已经在基友线程线程中拷贝过一份,基友线程循环时候读取的是基友线程 工作内存中的found变量,而这个found始终是false,程序死循环。我们称基友线程对我的线程更新found变量的行为是不可见的。

  如果found变量通过volatile进行修饰,基友线程修改found变量后,会立即将变量回写到主存中,并将我的线程里的found失效。我的线程发现自己变量失效后,会重新去主存中访问found变量,而此时的found变量已经变成true。循环退出。

  static volatile boolean found = false;

语义2、禁止指令冲排序:

  示例

 1 public class ReorderTest {
 2 
 3     private static int x = 0, y = 0;
 4     private static int a = 0, b = 0;
 5 
 6     public static void main(String[] args) throws InterruptedException {
 7         int i = 0;
 8         for (;;){
 9             i++;
10             x = 0; y = 0;
11             a = 0; b = 0;
12             Thread t1 = new Thread(new Runnable() {
13                 public void run() {
14                     a = 1;
15                     x = b;
16                 }
17             });
18 
19             Thread t2 = new Thread(new Runnable() {
20                 public void run() {
21                     b = 1;
22                     y = a;
23                 }
24             });
25 
26             t1.start();
27             t2.start();
28             t1.join();
29             t2.join();
30 
31             // 只有重排序的情况下,才会出现 0,0的结果
32             // 即x = b , y = a 比 a = 1,b = 1 先执行的情况下才会出现
33             if(x == 0 && y == 0) {
34                 String result = "第" + i + "次
 x=" + x + ", y=" + y + ", a=" + a + ", b=" + b;
35                 System.out.println(result);
36                 break;
37             } 
38         }
39     }
40 } 

  运行结果:第108861次  x=0, y=0, a=1, b=1

  上面的代码要出现x == 0 && y == 0的情况,只有保证 x = b 比 b = 1先执行,y = a 比 a = 1 先执行,才会出现,图解如下:

  

  那么t2中的代码放生了重排序,即指令重排序。如果加上volatile修饰 x、y、a、b变量之后,如下:

  private volatile static int x = 0, y = 0;

  private volatile static int a = 0, b = 0;

  程序永远不会结束,因为volatile禁止了指令重排序

语义3、volatile不保证原子性

  示例

 1 public class AtomicTest {
 2 
 3     private volatile static int counter = 0;
 4 
 5     public static void main(String[] args) {
 6 
 7         for (int i = 0; i < 10; i++) {
 8             Thread thread = new Thread(()->{
 9                 for (int j = 0; j < 10000; j++) {
10                       counter++;
11                 }
12                 System.out.println(Thread.currentThread().getName() + " Over~~~");
13             });
14             thread.start();
15         }
16 
17         try {
18             Thread.sleep(5000);
19         } catch (InterruptedException e) {
20             e.printStackTrace();
21         }
22 
23         System.out.println(counter);
24 
25     }
26 }

  重运行结果来看,它结果不等于100000,说明volatile不保证原子性

二、volatile的的底层实现

2.1、 Java代码层面

  上一段最简单的代码,volatile用来修饰Java变量

 1 public class TestVolatile {
 2 
 3     public static volatile int counter = 1;
 4 
 5     public static void main(String[] args){
 6         counter = 2;
 7         System.out.println(counter);
 8     }
 9 
10 }

2.2、字节码层面

  通过javac TestVolatile.java将类编译为class文件,再通过javap -v TestVolatile.class命令反编译查看字节码文件。

  打印内容过长,截图其中的一部分:

  

  可以看到,修饰counter字段的public、static、volatile关键字,在字节码层面分别是以下访问标志: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE

  volatile在字节码层面,就是使用访问标志:ACC_VOLATILE来表示,供后续操作此变量时判断访问标志是否为ACC_VOLATILE,来决定是否遵循volatile的语义处理。

2.3、JVM源码层面

  上小节图中main方法编译后的字节码,有putstaticgetstatic指令(如果是非静态变量,则对应putfieldgetfield指令)来操作counter字段。那么对于被volatile变量修饰的字段,是如何实现volatile语义的,从下面的源码看起。

  1、openjdk8根路径/hotspot/src/share/vm/interpreter路径下的bytecodeInterpreter.cpp文件中,处理putstaticputfield指令的代码:

 1 CASE(_putfield):
 2 CASE(_putstatic):
 3     {
 4           // .... 省略若干行 
 5           // ....
 6 
 7           // Now store the result 现在要开始存储结果了
 8           // ConstantPoolCacheEntry* cache;     -- cache是常量池缓存实例
 9           // cache->is_volatile()               -- 判断是否有volatile访问标志修饰
10           int field_offset = cache->f2_as_index();
11           if (cache->is_volatile()) { // ****重点判断逻辑**** 
12             // volatile变量的赋值逻辑
13             if (tos_type == itos) {
14               obj->release_int_field_put(field_offset, STACK_INT(-1));
15             } else if (tos_type == atos) {// 对象类型赋值
16               VERIFY_OOP(STACK_OBJECT(-1));
17               obj->release_obj_field_put(field_offset, STACK_OBJECT(-1));
18               OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
19             } else if (tos_type == btos) {// byte类型赋值
20               obj->release_byte_field_put(field_offset, STACK_INT(-1));
21             } else if (tos_type == ltos) {// long类型赋值
22               obj->release_long_field_put(field_offset, STACK_LONG(-1));
23             } else if (tos_type == ctos) {// char类型赋值
24               obj->release_char_field_put(field_offset, STACK_INT(-1));
25             } else if (tos_type == stos) {// short类型赋值
26               obj->release_short_field_put(field_offset, STACK_INT(-1));
27             } else if (tos_type == ftos) {// float类型赋值
28               obj->release_float_field_put(field_offset, STACK_FLOAT(-1));
29             } else {// double类型赋值
30               obj->release_double_field_put(field_offset, STACK_DOUBLE(-1));
31             }
32             // *** 写完值后的storeload屏障 ***
33             OrderAccess::storeload();
34           } else {
35             // 非volatile变量的赋值逻辑
36             if (tos_type == itos) {
37               obj->int_field_put(field_offset, STACK_INT(-1));
38             } else if (tos_type == atos) {
39               VERIFY_OOP(STACK_OBJECT(-1));
40               obj->obj_field_put(field_offset, STACK_OBJECT(-1));
41               OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
42             } else if (tos_type == btos) {
43               obj->byte_field_put(field_offset, STACK_INT(-1));
44             } else if (tos_type == ltos) {
45               obj->long_field_put(field_offset, STACK_LONG(-1));
46             } else if (tos_type == ctos) {
47               obj->char_field_put(field_offset, STACK_INT(-1));
48             } else if (tos_type == stos) {
49               obj->short_field_put(field_offset, STACK_INT(-1));
50             } else if (tos_type == ftos) {
51               obj->float_field_put(field_offset, STACK_FLOAT(-1));
52             } else {
53               obj->double_field_put(field_offset, STACK_DOUBLE(-1));
54             }
55           }
56           UPDATE_PC_AND_TOS_AND_CONTINUE(3, count);
57   }

  2、重点判断逻辑cache->is_volatile()方法,调用的是openjdk8根路径/hotspot/src/share/vm/utilities路径下的accessFlags.hpp文件中的方法,用来判断访问标记是否为volatile修饰

 1 // Java access flags
 2   bool is_public      () const         { return (_flags & JVM_ACC_PUBLIC      ) != 0; }
 3   bool is_private     () const         { return (_flags & JVM_ACC_PRIVATE     ) != 0; }
 4   bool is_protected   () const         { return (_flags & JVM_ACC_PROTECTED   ) != 0; }
 5   bool is_static      () const         { return (_flags & JVM_ACC_STATIC      ) != 0; }
 6   bool is_final       () const         { return (_flags & JVM_ACC_FINAL       ) != 0; }
 7   bool is_synchronized() const         { return (_flags & JVM_ACC_SYNCHRONIZED) != 0; }
 8   bool is_super       () const         { return (_flags & JVM_ACC_SUPER       ) != 0; }
 9   // 是否volatile修饰
10   bool is_volatile    () const         { return (_flags & JVM_ACC_VOLATILE    ) != 0; }
11   bool is_transient   () const         { return (_flags & JVM_ACC_TRANSIENT   ) != 0; }
12   bool is_native      () const         { return (_flags & JVM_ACC_NATIVE      ) != 0; }
13   bool is_interface   () const         { return (_flags & JVM_ACC_INTERFACE   ) != 0; }
14   bool is_abstract    () const         { return (_flags & JVM_ACC_ABSTRACT    ) != 0; }
15   bool is_strict      () const         { return (_flags & JVM_ACC_STRICT

  3、下面一系列的if...else...对tos_type字段的判断处理,是针对java基本类型和引用类型的赋值处理。如:

1 obj->release_byte_field_put(field_offset, STACK_INT(-1));

  对byte类型的赋值处理,调用的是openjdk8根路径/hotspot/src/share/vm/oops路径下的oop.inline.hpp文件中的方法:

1 // load操作调用的方法
2 inline jbyte oopDesc::byte_field_acquire(int offset) const                  
3 { return OrderAccess::load_acquire(byte_field_addr(offset));     }
4 // store操作调用的方法
5 inline void oopDesc::release_byte_field_put(int offset, jbyte contents)     
6 { OrderAccess::release_store(byte_field_addr(offset), contents); }

  赋值的操作又被包装了一层,又调用的OrderAccess::release_store方法。

   4、OrderAccess是定义在openjdk8根路径/hotspot/src/share/vm/runtime路径下的orderAccess.hpp头文件下的方法,具体的实现是根据不同的操作系统和不同的cpu架构,有不同的实现。

  强烈建议大家读一遍orderAccess.hpp文件中30-240行的注释!!!你就会发现本文1.2章所介绍内容的来源,也是网上各种雷同文章的来源。

  orderAccess_linux_x86.inline.hpp是linux系统下x86架构的实现:

  

  可以从上面看到,到c++的实现层面,又使用c++中的volatile关键字,用来修饰变量,通常用于建立语言级别的memory barrier。在《C++ Programming Language》一书中对volatile修饰词的解释:

  A volatile specifier is a hint to a compiler that an object may change its value in ways not specified by the language so that aggressive optimizations must be avoided.

  含义就是:

  • volatile修饰的类型变量表示可以被某些编译器未知的因素更改(如:操作系统,硬件或者其他线程等)
  • 使用 volatile 变量时,避免激进的优化。即:系统总是重新从内存读取数据,即使它前面的指令刚从内存中读取被缓存,防止出现未知更改和主内存中不一致

  5、步骤3中对变量赋完值后,程序又回到了2.3.1小章中第一段代码中一系列的if...else...对tos_type字段的判断处理之后。有一行关键的代码:OrderAccess::storeload(); 即:只要volatile变量赋值完成后,都会走这段代码逻辑。

  它依然是声明在orderAccess.hpp头文件中,在不同操作系统或cpu架构下有不同的实现。orderAccess_linux_x86.inline.hpp是linux系统下x86架构的实现:

  

  代码lock; addl $0,0(%%rsp) 其中的addl $0,0(%%rsp) 是把寄存器的值加0,相当于一个空操作(之所以用它,不用空操作专用指令nop,是因为lock前缀不允许配合nop指令使用)

  lock前缀,会保证某个处理器对共享内存(一般是缓存行cacheline,这里记住缓存行概念,后续重点介绍)的独占使用。它将本处理器缓存写入内存,该写入操作会引起其他处理器或内核对应的缓存失效。通过独占内存、使其他处理器缓存失效,达到了“指令重排序无法越过内存屏障”的作用

  内存屏障参考:【Java多线程】JMM(Java内存模型)(四)

  本文参考:https://zhuanlan.zhihu.com/p/133851347

原文地址:https://www.cnblogs.com/h--d/p/14174873.html