一、volatile是什么
volatile在java语言中是一个关键字,用于修饰变量。被volatile修饰的变量后,表示这个变量在不同线程中是共享,编译器与运行时都会注意到这个变量是共享的,因此不会对该变量进行重排序。
volatile关键字的两层语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,volatile变量具备以下特性:
-
具备可见性。一个线程对一个volatile变量的写对其他线程立马可见。
- 禁止一定的指令重排序。
-
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 + "次\n 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方法编译后的字节码,有putstatic
和getstatic
指令(如果是非静态变量,则对应putfield
和getfield
指令)来操作counter
字段。那么对于被volatile
变量修饰的字段,是如何实现volatile
语义的,从下面的源码看起。
1、openjdk8根路径/hotspot/src/share/vm/interpreter
路径下的bytecodeInterpreter.cpp
文件中,处理putstatic
和putfield
指令的代码:
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