关于探讨HotSpot中volatile的实现之前,需要先探讨一下Java的内存模型。为了提高程序的执行性能,编译器和处理器常常会对一段指令做重排序。在单线程环境下指令重排序可以显著提高程序的执行性能,但在多线程下便会出现一致性问题。Java通过统一的内存模型,锁,内存屏障等手段来处理这种一致性问题。事实上volatile在Java体系中是如此重要,AQS通过volatile的state变量和CAS辅助实现锁,是cucrrent包的基石。
一.Java内存模型
在Java中,堆内存在线程之间共享,局部变量不在线程间共享。JMM决定一个线程堆共享变量的写入何时对另一个线程可见,JMM定义了线程私有本地内存与主内存的抽象关系,提供统一的编程模型。线程间的共享变量存储在主内存中,线程私有内存存储了该线程读或写共享变量的副本,这是一个抽象模型,如图。
在这个统一抽象模型下,A线程和B线程同步共享数据时,A需要把自己的副本更新到主存,线程B去主存取值同时更新自己的副本。JMM通过控制主内存与每个线程本地内存交互来提供内存一致性。然而在多线程环境下上述一致性将被指令重排序扰乱。首先是编译器指令重排序只能保证不改变单线程程序语义可重排。处理器指令级并行技术(多级流水线技术)只能保证不改变单线程程序语义可重排。处理器缓存使得加载存储操作看上去乱序。
这些重排序会导致多线程下内存可见性问题。如何满足JMM内存一致性的要求,对于编译器,JMM编译器通过禁止某些特定类型的重排序。对于处理器,JMM要求处理器在生成Java指令序列时,插入特定的内存屏障指令。两种方式保障内存可见性。
1.内存屏障指令
处理器写缓冲临时保存向内存写入的数据,仅对它所在的处理器可见,处理器堆内存的读、写操作的执行顺序不一定与内存实际发生的读、写顺序一致。为了保证内存的可见性,java编译器在生成指令序列特定位置插入内存屏障指令来禁止特定类型的处理器重排序。
- LoadLoad:Load1;LoadLoad;Load2 确保Load1数据的装载早于Load2及所有后续装载指令。
- StoreStore:Store1;StoreStore;Store2 确保Store1数据对其他处理器可见(刷新到内存)早于Store2及后续存储指令。
- LoadStore:Load1;LoadStore;Store2 确保Load1数据装载早于Store2及后续存储指令。
- StoreLoad: Store1;StoreLoad;Load2 确保Store1存储数据对其他处理器可见早于Load2及后续加载指令。保证屏障之前的访内指令完成之后,再继续后续指令。
2.先行发生规则
- 程序顺序规则:一个线程中的每个操作先行于该线程的任意后续操作。
- 监视器规则:监视器解锁优先于加锁。
- volatile规则:对volatie变量的写先于读。
- 传递性:A>B,B>C => A>C。
3.顺序一致性
未正确的同步会导致数据竞争。JMM通过同步来保证内存一致性。关于同步前两篇已经介绍了。顺序一致性内存模型中一个线程中的所有操作必须被顺序致性,不管是否同步每个操作必须原子致性立即对所有线程可见。
A线程的三个操作执行后释放监视器锁,随后B线程获取同一个监视器锁
如果不同步,在顺序一致模型中整体执行顺序是无序的
public class SynchronizedApp {
int a = 0;
boolean flag = false;
public synchronized void writer(){ //获取锁
a = 1;
flag = true;
} //释放锁
public synchronized void reader(){ //获取锁
if (flag){
int i = a;
}
} //释放锁
}
上述代码中线程A执行writer()方法后,B线程执行reader()方法,是正确同步的。执行结果与顺序一致性模型中的执行结果相同。
顺序一致性模型中所有操作完全按程序的顺序串行执行。JMM内临界区的代码可以重排序,通过在进入退出临界区进行特殊处理,使得与顺序一致内存模型和相同。在不改变程序执行结果的前提下,尽可能为编译器处理器优化保留余地。
4.volatile
理解volatile特性可以把它看成使用同一个锁对单个读写操作进行了同步,volatile特性:
- 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入
- 原子性:对任意单个volatile变量的读或写具有原子性但是对于volatile++这种复合操作不具有原子性
public class VolatileApp {
volatile long val = 0L; //申明long变量
public void set(long l){
val = l; //单个volatile变量的写
}
public void getAndIncrement(){
val++; //多个volatile变量的读写
}
public long get(){
return val; //单个volatile变量读
}
}
JSR-133开始,volatile变量的读写可实现线程间的通讯,内存语义上来说volatile变量的读写与锁的获取释放有相同的效果。volatile写和锁释放有相同的内存语义;volatile读与锁获取有相同的语义。如下
public class VolatileSynchronizedApp {
int a = 0;
volatile boolean flag = false;
public void writer(){
a = 1; //1
flag = true; //2
}
public void reader(){
if (flag){ //3
int i = a; //4
}
}
}
A线程执行writer()方法后,B线程执行reader()方法,由先行发生原则:
- 程序执行顺序:1 > 2; 3 > 4
- volatile规则: 2 > 3
- 先行原则传递: 1 > 4
A线程写一个votalie变量后(等同于释放锁),B线程读同一个volitile变量(等同加锁)。A在写volatile变量之前所有可见的共享变量在B读的时候将立即对B可见。
4. 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
5. 当读一个volatile变量时JMM会把该线程对应的本地内存置为无效,从主内存读取共享变量。
为了实现volatile的内存语义,编译器在生成字节码时会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,采用保守策略:
- 每个volatile写操作的前面插入一个StoreStore屏障
- 每个volatile写操作的后面面插入一个StoreLoad屏障
3. 每个volatile读操作的后面面插入一个LoadLoad屏障(没错是后面)
4. 每个volatile写操作的后面面插入一个StoreStore屏障(没错是后面)
二.volatile的实现
javap -v VolatileApp.class 查看字节码, 从编译器层面可以看到多出了ACC_VOLATILE 标志,volatile变量的读写通过getfield和putfield并没有特殊之处
{
volatile long val;
descriptor: J
flags: (0x0040) ACC_VOLATILE //volarile标志
public void set(long);
descriptor: (J)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=3, args_size=2
0: aload_0
1: lload_1
2: putfield #2 // Field val:J
5: return
LineNumberTable:
line 8: 0
line 9: 5
public long get();
descriptor: ()J
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field val:J
4: lreturn
LineNumberTable:
line 16: 0
}
X86平台下,getfield和putfield的字节码模板指令如下
hotspot/src/cpu/x86/vm/templateTable_x86.cpp
void TemplateTable::getfield(int byte_no) {
getfield_or_static(byte_no, false);
}
hotspot/src/cpu/x86/vm/templateTable_x86.cpp
_getstatic / _getfield适用于所有类型的字段属性读取,因此在具体实现时需要根据flags中保存的属性类型适配对应的处理逻辑,为了避免每次都要判断属性类型,OpenJDK增加了几个自定义的带目标类型的属性读取的字节码指令,如_fast_igetfield
void TemplateTable::getfield_or_static(int byte_no, bool is_static, RewriteControl rc) {
transition(vtos, vtos);
//加载该字段的偏移量,flags,如果是静态字段还需要解析该类class实例对应的oop
load_field_cp_cache_entry(obj, cache, index, off, flags, is_static);
//将被读取属性的oop放入obj中
if (!is_static) pop_and_check_object(obj);
__ andl(flags, ConstantPoolCacheEntry::tos_state_mask);
//boolean变量
__ jcc(Assembler::notZero, notByte);
// btos
__ load_signed_byte(rax, field);
__ push(btos);
// //将该指令改写成_fast_bgetfield,下一次执行时就是_fast_bgetfield
if (!is_static && rc == may_rewrite) {
patch_bytecode(Bytecodes::_fast_bgetfield, bc, rbx);
}
__ jmp(Done);
......
__ bind(Done);
// [jk] not needed currently
// volatile_barrier(Assembler::Membar_mask_bits(Assembler::LoadLoad |
// Assembler::LoadStore));
}
hotspot/src/cpu/x86/vm/templateTable_x86.cpp
void TemplateTable::putfield(int byte_no) {
putfield_or_static(byte_no, false);
}
hotspot/src/cpu/x86/vm/templateTable_x86.cpp
void TemplateTable::putfield_or_static(int byte_no, bool is_static, RewriteControl rc) {
transition(vtos, vtos);
//找到该属性对应的ConstantPoolCacheEntry
resolve_cache_and_index(byte_no, cache, index, sizeof(u2));
//发布事件
jvmti_post_field_mod(cache, index, is_static);
//获取字段偏移量,flags,如果是静态属性获取对应类的class实例
load_field_cp_cache_entry(obj, cache, index, off, flags, is_static);
//取flags到rdx
__ movl(rdx, flags);
__ shrl(rdx, ConstantPoolCacheEntry::is_volatile_shift);
__ andl(rdx, 0x1);
......
//判断是否volatile变量,
__ testl(rdx, rdx);
__ jcc(Assembler::zero, notVolatile);
volatile_barrier(Assembler::Membar_mask_bits(Assembler::StoreLoad |
Assembler::StoreStore));
__ bind(notVolatile);
}
hotspot/src/cpu/x86/vm/templateTable_x86.cpp
void TemplateTable::volatile_barrier(Assembler::Membar_mask_bits order_constraint ) {
// Helper function to insert a is-volatile test and memory barrier
if(!os::is_MP()) return; // Not needed on single CPU
__ membar(order_constraint);
}
hotspot/src/cpu/x86/vm/assembler_x86.hpp
enum Membar_mask_bits {
StoreStore = 1 << 3,
LoadStore = 1 << 2,
StoreLoad = 1 << 1,
LoadLoad = 1 << 0
};
hotspot/src/cpu/x86/vm/assembler_x86.hpp
如果是volatile变量,在属性修改完成后就会执行lock addl $0×0,(%rsp),执行lock指令会将对高速缓存行的修改回写到主内存中,同时通过缓存一致性协议通知其他CPU的高速缓存控制器将相关变量的高速缓存行置为无效,当其他CPU再次读取该缓存行时发现该缓存行是无效的,就会重新从主内存加载该变量到高速缓存行中,从而实现对其他CPU的可见性
// Serializes memory and blows flags
void membar(Membar_mask_bits order_constraint) {
if (os::is_MP()) {
//只处理StoreLoad
if (order_constraint & StoreLoad) {
//所有可用的芯片都支持“锁定”指令,这足以作为屏障
int offset = -VM_Version::L1_line_size();
if (offset < -128) {
offset = -128;
}
//向总线发出lock add 指令,同步内存
lock();
addl(Address(rsp, offset), 0);// Assert the lock# signal here
}
}
}
编译器和处理器对内存模型的实现。编译器和处理器希望内存模型对他们的约束越少越好。这样就可以尽可能多的优化。JMM希望提供给程序员一个强一致性的内存模型。通过内存屏障,先行发生原则,顺序一致性,从虚拟机,编译器,编程语言层面来实现这个一致性的内存模型。JMM保证了程序在任意处理器平台上的执行结果与该程序在顺序一致性内存模型中执行结果的一致性。