阿里让我讲讲volatile,我直接从HotSpot开始讲起
你好,我是小黄,一名独角兽企业的Java开发工程师。感谢茫茫人海中我们能够相遇, 俗话说:当你的才华和能力,不足以支撑你的梦想的时候,请静下心来学习 希望优秀的你可以和我一起学习,一起努力,实现属于自己的梦想。
一、引言
对于Java开发者而言,关于底层知识,我们一般当做黑盒来进行使用,不需要去打开这个黑盒。
但随着目前程序员行业的发展,我们有必要打开这个黑盒,去探索其中的奥妙。
本篇系列文章,将带你一起探索底层黑盒的奥秘之处。
阅读本篇文章之前,建议下载 openJDK
效果会更好
openJDK下载地址:openJDK
如果感觉网速较慢,建议关注公众号:爱敲代码的小黄,发送:openJDK
即可获取百度网盘链接。
大家可不可以给我点个关注呀~
二、操作系统
1、CPU的乱序执行
CPU在进行读等待的同时执行指令,是CPU乱序的根源,不是乱,而是提高效率
我们来看下面这个程序:
x = 0; y = 0; a = 0; b = 0; Thread one = new Thread(new Runnable() { public void run() { //由于线程one先启动,下面这句话让它等一等线程two. 可根据自己电脑的实际性能适当调整等待时间. //shortWait(100000); a = 1; x = b; } }); Thread other = new Thread(new Runnable() { public void run() { b = 1; y = a; } }); one.start(); other.start(); one.join(); other.join(); String result = "第" + i + "次 (" + x + "," + y + ")"; if (x == 0 && y == 0) { System.err.println(result); break; }
我们可以看到,如果我们的CPU没有乱序执行的话,那么 a = 1
必然在 x = b
前面,b = 1
必然在 y = a
的前面
我们可以得到什么结论,也就是 x
和 y
肯定不能同时为 0
(这里读者可以好好想一想,为什么不能同时为0)
我们运行下程序,得到如下结果:
我们在运行 2728842
次的时候,得到了该结果,验证了我们的结论。
2.1 乱序可能会出现的问题
常见例子:DCL为什么要加 volatile
?
我们以下面举例:
class T{ int m = 8; } T t = new T();
反编译汇编码:
0 new #2 <T> 3 dup 4 invokespeecial # 3 <T.<init>> 7 astore_1 8 return
我们对于汇编码逐步分析:
new #2 <T>
:创建m = 0
的对象并且栈帧中有一个引用指向该对象
dup
:在我们的栈帧中复制一份引用
invokespecial #3 <T.<init>>
:弹出一个栈帧中的值,实例化他的构造方法
astore_1
:将我们栈帧的引用赋值给 t,这里1
指的是我们本地变量表中的第一位
所以,我们想一个事情,上面我们已经证明CPU存在乱序的现象,那么会对我们的上述操作有什么危害呢?
当我们的 astore_1
在我们的 invokespeecial # 3 <T.<init>>
执行前执行,会导致我们的将我们没有实例化的对象赋值给 t
,如下如所示:
所以为了避免这种现象,我们要对 DCL
加 volatile
问题来了,我们 volatile
是怎么保证有序性的呢?
2.2 如何禁止指令重排序
我们对于禁止指令重排序,从以下三方面来谈:
- 代码层面
- 字节码层面
- JVM层面
- CPU层面
2.2.1 Java 代码层面
直接加一个
volatile
关键字即可public class TestVolatile { public static volatile int counter = 1; public static void main(String[] args) { counter = 2; System.out.println(counter); } }
2.2.1 字节码层面
在字节码层面,当对 volatile
进行反编译后,我们可以看到 VCC_volatile
我们对上述代码进行反编译,得到其 字节码
通过 javac TestVolatile.java
将类编译为class文件,再通过 javap -v TestVolatile.class
命令反编译查看字节码文件
这里我们只展示这段代码得字节码:public static volatile int counter = 1;
public static volatile int counter; descriptor: I flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE // 下面为初始化counter时的字节码 0: iconst_2 1: putstatic #2 // Field counter:I 4: getstatic #3 // Field
descriptor:代表方法参数和返回值
flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE:标志
putstatic:对静态属性进行操作
我们后续的操作都可以通过 ACC_VOLATILE
这个标志来知道该变量已被 volatile
所修饰
2.2.3 HotSpot 源码层面
对于带有 volatile
修饰的变量,我们的 JVM
是怎么去实现的呢?
通常我们在我网站上会看到这四个词语:StoreStore
,StoreLoad
,LoadStore
,LoadLoad
我们的 JVM
确实是这样实现的,我们一起来看一下具体的实现吧。
Java中,静态属性属于类的。操作静态属性,对应的指令为 putstatic
我们以 openjdk8
根路径 jdk\src\hotspot\share\interpreter\zero
路径下的 bytecodeInterpreter.cpp
文件中,处理 putstatic
指令的代码:
CASE(_putstatic): { // .... 省略若干行 // Now store the result 现在要开始存储结果了 // ConstantPoolCacheEntry* cache; -- cache是常量池缓存实例 // cache->is_volatile() -- 判断是否有volatile访问标志修饰 int field_offset = cache->f2_as_index(); // ****重点判断逻辑**** if (cache->is_volatile()) { // volatile变量的赋值逻辑 if (tos_type == itos) { obj->release_int_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == atos) {// 对象类型赋值 VERIFY_OOP(STACK_OBJECT(-1)); obj->release_obj_field_put(field_offset, STACK_OBJECT(-1)); OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0); } else if (tos_type == btos) {// byte类型赋值 obj->release_byte_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == ltos) {// long类型赋值 obj->release_long_field_put(field_offset, STACK_LONG(-1)); } else if (tos_type == ctos) {// char类型赋值 obj->release_char_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == stos) {// short类型赋值 obj->release_short_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == ftos) {// float类型赋值 obj->release_float_field_put(field_offset, STACK_FLOAT(-1)); } else {// double类型赋值 obj->release_double_field_put(field_offset, STACK_DOUBLE(-1)); } // *** 写完值后的storeload屏障 *** OrderAccess::storeload(); } else { // 非volatile变量的赋值逻辑 } }
这里贴一下 cache->is_volatile()
的源码,路径:jdk\src\hotspot\share\utilities\accessFlags.hpp
// Java access flags bool is_public () const { return (_flags & JVM_ACC_PUBLIC ) != 0; } bool is_private () const { return (_flags & JVM_ACC_PRIVATE ) != 0; } bool is_protected () const { return (_flags & JVM_ACC_PROTECTED ) != 0; } bool is_static () const { return (_flags & JVM_ACC_STATIC ) != 0; } bool is_final () const { return (_flags & JVM_ACC_FINAL ) != 0; } bool is_synchronized() const { return (_flags & JVM_ACC_SYNCHRONIZED) != 0; } bool is_super () const { return (_flags & JVM_ACC_SUPER ) != 0; } bool is_volatile () const { return (_flags & JVM_ACC_VOLATILE ) != 0; } bool is_transient () const { return (_flags & JVM_ACC_TRANSIENT ) != 0; } bool is_native () const { return (_flags & JVM_ACC_NATIVE ) != 0; } bool is_interface () const { return (_flags & JVM_ACC_INTERFACE ) != 0; } bool is_abstract () const { return (_flags & JVM_ACC_ABSTRACT ) != 0; }
我们看一下赋值 obj->release_long_field_put(field_offset, STACK_LONG(-1))
的源代码:jdk\src\hotspot\share\oops\oop.inline.hpp
jlong oopDesc::long_field_acquire(int offset) const { return Atomic::load_acquire(field_addr<jlong>(offset)); } void oopDesc::release_long_field_put(int offset, jlong value) { Atomic::release_store(field_addr<jlong>(offset), value); }
我们前往 jdk\src\hotspot\share\runtime\atomic.hpp
看一下 Atomic::release_store
的方法
inline T Atomic::load_acquire(const volatile T* p) { return LoadImpl<T, PlatformOrderedLoad<sizeof(T), X_ACQUIRE> >()(p); } template <typename D, typename T> inline void Atomic::release_store(volatile D* p, T v) { StoreImpl<D, T, PlatformOrderedStore<sizeof(D), RELEASE_X> >()(p, v); }
我们可以清楚的看到,const volatile T* p
和 volatile D* p
在调用的时候,直接使用了 C/C++
的 volatile
关键字
我们继续往下看,在我门执行完参数的赋值后,会有这个一个操作:OrderAccess::storeload();
我们观察 jdk\src\hotspot\share\runtime
的 orderAccess.hpp
文件,发现有这么一段代码
// barriers 屏障 static void loadload(); static void storestore(); static void loadstore(); static void storeload(); static void acquire(); static void release(); static void fence();
我们可以清楚的看到,这就是我们在各大网站看到的 JVM
的读写屏障
当然,我们还要看其在 linux_x86
实现方式,在 jdk\src\hotspot\os_cpu\linux_x86
的 orderAccess_linux_x86.hpp
下
// A compiler barrier, forcing the C++ compiler to invalidate all memory assumptions static inline void compiler_barrier() { __asm__ volatile ("" : : : "memory"); } inline void OrderAccess::loadload() { compiler_barrier(); } inline void OrderAccess::storestore() { compiler_barrier(); } inline void OrderAccess::loadstore() { compiler_barrier(); } inline void OrderAccess::storeload() { fence(); } inline void OrderAccess::acquire() { compiler_barrier(); } inline void OrderAccess::release() { compiler_barrier(); }
2.2.4 CPU层面
- Intel 的原语指令:
mfence内存屏障
,ifence读屏障
,sfence写屏障
我们可以看到,最关键的是这一行代码:__asm__ volatile ("" : : : "memory");
__asm__ :用于指示编译器在此插入汇编语句
volatile :告诉编译器,严禁将此处的汇编语句与其它的语句重组合优化。即:原原本本按原来的样子处理这这里的汇编。
("" : : : "memory"):memory 强制 gcc 编译器假设 RAM 所有内存单元均被汇编指令修改,这样 cpu 中的 registers 和 cache 中已缓存的内存单元中的数据将作废。cpu 将不得不在需要的时候重新读取内存中的数据。这就阻止了 cpu 又将 registers, cache 中的数据用于去优化指令,而避免去访问内存。
简单概括:告诉我们的CPU,别瞎几把给我优化了,我就要串行执行。
这样我们可以看到,这些指令都是通过更改CPU的 寄存器 和 缓存 来保持有序性的
到这基本面试就差不多了,能打败 80%
面试者和面试官了,但我们这篇文章还不够!
我们观察这些方***发现有一个叫 fence()
的方法,我们观察一下这个方法:
inline void OrderAccess::fence() { // always use locked addl since mfence is sometimes expensive // #ifdef AMD64 __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory"); #else __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory"); #endif compiler_barrier(); }
我们可以看到,我们的方法不建议我们使用我们的原语指令 mfence(内存屏障)
,因为 mfence
的资源消耗要比 locked
资源消耗的多
直接判断是不是 AMD64
来对其不同的寄存器 rsp\esp
做处理
"lock; addl $0,0(%%rsp)":在 rsp 寄存器上加一个 0) 指令是一个 Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个 CPU
到这里,我们的 volatile
基本差不多了,应该可以通过打败 90%
的面试官了
2.3 hanppens-before原则
简单来说,JVM规定重排序必须遵守的规则(了解即可)
- 程序次序规则
- 管程锁定规则
- volatile
- 线程启动规则
- 线程终止规则
- 线程中断规则
- 对象终结规则
- 传递性
2.4 as if serial
不管如何重排序,单线程执行的结果不会改变
三、总结
这一篇文章写了大概一个星期,最难的地方在于一直找不到一个从浅入深的过程,导致自己一直不知道该怎么写
最终还是成功的完成了,让自己对于 volatile
的理解又进了一步
至少看完这篇文章,在 volatile
的问题上,不惧怕任何面试官
下一步准备讲讲 合并写
、进程、线程、纤程
或者 算法
我是一名独角兽企业的Java开发工程师,希望 聪明、可爱、善良的你 可以点个关注呀,有问题可以留言或者私信我们下期再见!
#Java##Java工程师#