分析volatile的做用以及底层实现原理,这也是大公司喜欢问的问题java
在多线程中,volatile和synchronized都起到很是重要的做用,synchronized是经过加锁来实现线程的安全性。而volatile的主要做用是在多处理器开发中保证共享变量对于多线程的可见性。
可见性的意思是,当一个线程修改一个共享变量时,另一个线程能读取到修改之后的值。接下来经过一个简单的案例来演示可见性问题linux
public class VolatileDemo { private /*volatile*/ static boolean stop=false; //添加volatile修饰和不添加volatile修饰的演示效果 public static void main(String[] args) throws InterruptedException { Thread thread=new Thread(()->{ int i=0; while(!stop){ i++; } }); thread.start(); System.out.println("begin start thread"); Thread.sleep(1000); stop=true; } }
这段代码有些人测试不出效果,是由于JVM没有优化致使的,在cmd控制台输入java -version,若是显示的是 JavaHotSpot(TM)ServerVM,就能正常演示,若是是 JavaHotSpot(TM)ClientVM,须要设置成 Server模式c++
什么是可见性,以及volatile是如何保证可见性的呢?数据库
在并发编程中,线程安全问题的本质其实就是 原子性、有序性、可见性;接下来主要围绕这三个问题进行展开分析其本质,完全了解可见性的特性编程
原子性、有序性、可见性这些问题,咱们能够认为是基于多核心CPU架构下的存在的问题。由于在单核CPU架构下,全部的线程执行都是基于CPU时间片切换,因此不存在并发问题 (在IntelPentium4开始,引入了超线程技术,也就是一个CPU核心模拟出2个线程的CPU,实现多线程并行)。数组
线程设计的目的是充分利用CPU达到实时性的效果,可是不少时候CPU的计算任务还须要和内存进行交互,好比读取内存中的运算数据、将处理结果写入到内存。在理想状况下,存储器应该是很是快速的执行一条指令,这样CPU就不会受到存储器的限制。但目前技术没法知足,因此就出现了其余的处理方式。缓存
图片描述安全
存储器顶层是CPU中的寄存器,存储容量小,可是速度和CPU同样快,因此CPU在访问寄存器时几乎没有延迟;接下来就是CPU的高速缓存;最后就是内存。多线程
图片描述架构
高速缓存从下到上越接近CPU访问速度越快,同时容量也越小。如今的大部分处理器都有二级或者三级缓存,分别是L1/L2/L3, L1又分为L1-d的数据缓存和L1-i的指令缓存。其中L3缓存是在多核CPU之间共享的。
在多核CPU架构下,在同一时刻对同一共享变量执行 decl指令(递减指令,至关于i--,它分为三个过程:读->改->写
,这个指令涉及到两次内存操做,那么在这种状况下i的结果是没法预测的。这就是原子性问题
处理器如何解决原子性问题呢?
其实这个问题稍微提炼一下,无非就是多线程并行访问同一个共享资源的时候的原子性问题,若是把问题放大到分布式架构里面,这个问题的解决方法就是锁。因此在CPU层面,提供了两种锁的机制来保证原子性
若是多个处理器同时对同一共享变量进行 decl指令操做,那这个操做必定不是原子的,也就是执行的结果和预期结果不一致。以下图所示,咱们指望的结果是3,可是有可能结果是2
图片描述
若是要解决这个问题,就须要是的CPU0在更新共享变量时,CPU1就不能操做缓存了该共享变量内存地址的缓存,因此处理器提供了总线锁来解决问题,处理器会提供一个LOCK#信号,当一个处理器在总线上输出这个信号时,其余处理器的请求会被阻塞,那么该处理器就能够独占共享内存
总线锁有一个弊端,总线锁至关于使得多个CPU由并行执行变成了串行,使得CPU的性能严重降低,因此在P6系列之后的处理器中,引入了缓存锁。
咱们只须要保证 多个线程操做同一个被缓存的共享数据的原子性就行,因此只须要锁定被缓存的共享对象便可。所谓缓存锁是指被缓存在处理器中的共享数据,在Lock操做期间被锁定,那么当被修改的共享内存的数据回写到内存时,处理器不在总线上声明LOCK#信号,而是修改内部的内存地址,并经过 缓存一致性机制来保证操做的原子性。
什么是缓存一致性呢?
所谓缓存一致性,就是多个CPU核心中缓存的同一共享数据的数据一致性,而(MESI)使用比较普遍的缓存一致性协议。MESI协议其实是表示缓存的四种状态
M(Modify) 表示共享数据只缓存在当前CPU缓存中,而且是被修改状态,也就是缓存的数据和主内存中的数据不一致
E(Exclusive) 表示缓存的独占状态,数据只缓存在当前CPU缓存中,而且没有被修改
S(Shared) 表示数据可能被多个CPU缓存,而且各个缓存中的数据和主内存数据一致
I(Invalid) 表示缓存已经失效
每一个CPU核心不只仅知道本身的读写操做,也会监听其余Cache的读写操做
CPU的读取会遵循几个原则
CPU高速缓存以及指令重排序都会形成可见性问题,接下来从两个角度来分析
前面说过MESI协议,也就是缓存一致性协议。这个协议存在一个问题,就是当CPU0修改当前缓存的共享数据时,须要发送一个消息给其余缓存了相同数据的CPU核心,这个消息传递给其余CPU核心以及收到消息完成各自缓存状态的切换这个过程当中,CPU会等待全部缓存响应完成,这样会下降处理器的性能。为了解决这个问题,引入了 StoreBufferes存储缓存。
处理器把须要写入到主内存中的值先写入到存储缓存中,而后继续去处理其余指令。当全部的CPU核心返回了失效确认时,数据才会被最终提交。可是这种优化又会带来另外的问题。
若是某个CPU尝试将其余CPU占有的共享数据写入到内存,消息提交给store buffer之后,当前CPU继续作其余事情,而若是后面的指令依赖于这个被写入内存的最新数据(因为store buffer尚未写入到内存),就会产生可见性问题(也就是值尚未更新到内存中,这个时候读取到的共享数据的值是错误的)。
Store Bufferes中的数据什么时候写入到内存中是不肯定的,那么意味着这个过程的执行顺序也是不肯定的,好比下面这个例子
exeToCPU0和exeToCPU1分别在两个独立的cpu核心上执行,假如CPU0 缓存了 isFinish这个共享变量,而且状态为(E->独占),而value多是(S共享状态被其余CPU核心修改之后变为I(失效状态)。
这种状况下value的缓存数据变动路径为, value将失效状态须要响应给触发缓存更新的CPU核心,接着该CPU将 StoreBufferes写入到内存,这就会致使value会比isFinish更迟的抛弃存储缓存。那么就可能出现CPU1读取到了isFinish的值为true,而value的值不等于10的状况。
这种CPU的内存乱序访问,会带来可见性问题。
value = 3; void exeToCPU0(){ value = 10; isFinsh = true; } void exeToCPU1(){ if(isFinsh){ assert value == 10; } }
什么是内存屏障?从前面的内容基本能有一个初步的猜测,内存屏障就是将 store bufferes中的指令写入到内存,从而使得其余访问同一共享内存的线程的可见性。
X86的memory barrier指令包括lfence(读屏障) sfence(写屏障) mfence(全屏障)
Store Memory Barrier(写屏障) 告诉处理器在写屏障以前的全部已经存储在存储缓存(store bufferes)中的数据同步到主内存,简单来讲就是使得写屏障以前的指令的结果对屏障以后的读或者写是可见的
Load Memory Barrier(读屏障) 处理器在读屏障以后的读操做,都在读屏障以后执行。配合写屏障,使得写屏障以前的内存更新对于读屏障以后的读操做是可见的
Full Memory Barrier(全屏障) 确保屏障前的内存读写操做的结果提交到内存以后,再执行屏障后的读写操做
有了内存屏障之后,对于上面这个例子,咱们能够这么来改,从而避免出现可见性问题
value = 3; void exeToCPU0(){ value = 10; storeMemoryBarrier(); //这个是一个伪代码,插入一个写屏障,使得value=10这个值强制写入到主内存中 isFinsh = true; } void exeToCPU1(){ if(isFinsh){ loadMemoryBarrier();//伪代码,插入一个读屏障,使得cpu1从主内存中得到最新的数据 assert value == 10; } }
总的来讲,内存屏障的做用能够经过防止CPU对内存的乱序访问来保证共享数据在多线程并行执行下的可见性
有序性简单来讲就是程序代码执行的顺序是否按照咱们编写代码的顺序执行,通常来讲,为了提升性能,编译器和处理器会对指令作重排序,重排序分3类
也就是说,咱们编写的源代码到最终执行的指令,会通过三种重排序
有序性会带来可见性问题,因此能够经过内存屏障指令来进制特定类型的处理器重排序
从硬件层面的分析了解到原子性、有序性、可见性的本质之后,知道硬件层面针对这三个问题的解决办法,原子性是经过总线锁或缓存锁来实现,而有序性和可见性能够经过内存屏障来解决。那么在软件层面,如何解决原子性、有序性、可见性问题呢?答案就是: JMM(JavaMemoryModel)内存模型
硬件层面的原子性、有序性、可见性在不一样的CPU架构和操做系统中的实现可能都不同,而Java语言的特性是 write once,run anywhere,意味着JVM层面须要屏蔽底层的差别,所以在JVM规范中定义了JMM。
(JMM内存模型的抽象结构)
JMM属于语言级别的抽象内存模型,能够简单理解为对硬件模型的抽象,它定义了共享内存中多线程程序读写操做的行为规范,也就是在虚拟机中将共享变量存储到内存以及从内存中取出共享变量的底层细节。
经过这些规则来规范对内存的读写操做从而保证指令的正确性,它解决了CPU多级缓存、处理器优化、指令重排序致使的内存访问问题,保证了并发场景下的可见性。
须要注意的是,JMM并无限制执行引擎使用处理器的寄存器或者高速缓存来提高指令执行速度,也没有限制编译器对指令进行重排序,也就是说在JMM中,也会存在缓存一致性问题和指令重排序问题。只是JMM把底层的问题抽象到JVM层面,再基于CPU层面提供的内存屏障指令,以及限制编译器的重排序来解决并发问题
Java内存模型定义了线程和内存的交互方式,在JMM抽象模型中,分为主内存、工做内存;主内存是全部线程共享的,通常是实例对象、静态字段、数组对象等存储在堆内存中的变量。工做内存是每一个线程独占的,线程对变量的全部操做都必须在工做内存中进行,不能直接读写主内存中的变量,线程之间的共享变量值的传递都是基于主内存来完成。
在JMM中,定义了8个原子操做来实现一个共享变量如何从主内存拷贝到工做内存,以及如何从工做内存同步到主内存,交互以下
8个原子操做指令
lock(锁定):做用于主内存的变量,把一个变量标识为一条线程独占状态。
unlock(解锁):做用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才能够被其余线程锁定。
read(读取):做用于主内存变量,把一个变量值从主内存传输到线程的工做内存中,以便随后的load动做使用
load(载入):做用于工做内存的变量,它把read操做从主内存中获得的变量值放入工做内存的变量副本中。
use(使用):做用于工做内存的变量,把工做内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个须要使用变量的值的字节码指令时将会执行这个操做。
assign(赋值):做用于工做内存的变量,它把一个从执行引擎接收到的值赋值给工做内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操做。
store(存储):做用于工做内存的变量,把工做内存中的一个变量的值传送到主内存中,以便随后的write的操做。
write(写入):做用于主内存的变量,它把store操做从工做内存中一个变量的值传送到主内存的变量中。
若是要把一个变量从主内存中复制到工做内存,就须要按顺寻地执行read和load操做,若是把变量从工做内存中同步回主内存中,就要按顺序地执行store和write操做。JMM只要求这两个操做必须按顺序执行,而没有保证必须是连续执行。也就是read和load之间,store和write之间是能够插入其余指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a。
JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致,由于若是想要保证执行结果一致,意味着JMM须要进制处理器和编译器的优化,这对于程序的执行性能会产生很大的影响。因此在未同步程序的执行中,因为执行顺序的不肯定性致使结果没法预测。咱们可使用同步原语好比 synchronized,volatile、final来实现程序的同步操做来保证顺序一致性
假若有两个线程A和B并行执行,A和B线程分别都有3个操做,在程序中的顺序是 A1->A2->A3, B1->B2->B3。
假设这两个程序没有使用同步原语,那么线程并行执行的效果多是
此图来自并发编程的艺术
若是这两个程序使用了监视器锁来实现正确同步,那么执行的过程必定是
此图来自并发编程的艺术
CPU层面的内存乱序访问属于重排序的一部分,同时咱们还提到了编译器的优化执行的重排序。重排序是一种优化手段,可是在多线程并发中,会致使可见性问题。
编译器的重排序是指,在不改变单线程程序语义的前提下,能够从新安排语句的执行顺序来优化程序的性能.
编译器的重排序和CPU的重排序的原则同样,会遵照数据依赖性原则,编译器和处理器不会改变存在数据依赖关系的两个操做的执行顺序,好比下面的代码,这三种状况在单线程里面若是改变代码的执行顺序,都会致使结果不一致,因此重排序不会对这类的指令作优化,也就是须要知足 as-if-serial语义
//写后读 a=1; b=1; //写后写 a=1; a=2; //读后写 a=b; b=1;
as-if-serial语义
as-if-serial语义的意思是无论怎么重排序,单线程程序的执行结果不能被改变,编译器、处理器都必须遵照这个语义
JMM层面的内存屏障
为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障来禁止特定类型的处理器的重排序,在JMM中把内存屏障分为四类
屏障的做用这里就不重复再说了,实际上JMM层面的内存屏障就是对CPU层面的内存屏障指令作的包装,做用是经过在合适的位置插入内存屏障来保证可见性
相信经过上面的分析,基本上有了答案
若是你看到这个章节了,意味着你对可见性有一个清晰的认识了,也知道JMM是基于禁止指令重排序来实现可见性的,那么咱们再来分析volatile的源码,就会简单不少
基于最开始演示的这段代码做为入口
public class VolatileDemo { public volatile static boolean stop=false; public static void main(String[] args) throws InterruptedException { Thread thread=new Thread(()->{ int i=0; while(!stop){ i++; } }); thread.start(); System.out.println("begin start thread"); Thread.sleep(1000); stop=true; } }
经过 javap-vVolatileDemo.class
查看字节码指令
public static volatile boolean stop; descriptor: Z flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE ...//省略 public static void main(java.lang.String[]) throws java.lang.InterruptedException; descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=2, args_size=1 0: new #2 // class java/lang/Thread 3: dup 4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable; 9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V 12: astore_1 13: aload_1 14: invokevirtual #5 // Method java/lang/Thread.start:()V 17: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 20: ldc #7 // String begin start thread 22: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 25: ldc2_w #9 // long 1000l 28: invokestatic #11 // Method java/lang/Thread.sleep:(J)V 31: iconst_1 32: putstatic #12 // Field stop:Z 35: return
注意被修饰了volatile关键字的 stop字段,会多一个 ACC_VOLATILE的flag,在给 stop复制的时候,调用的字节码是 putstatic,这个字节码会经过BytecodeInterpreter
解释器来执行,找到Hotspot的源码 bytecodeInterpreter.cpp
文件,搜索 putstatic指令定位到代码
CASE(_putstatic): { u2 index = Bytes::get_native_u2(pc+1); ConstantPoolCacheEntry* cache = cp->entry_at(index); if (!cache->is_resolved((Bytecodes::Code)opcode)) { CALL_VM(InterpreterRuntime::resolve_get_put(THREAD, (Bytecodes::Code)opcode), handle_exception); cache = cp->entry_at(index); } #ifdef VM_JVMTI if (_jvmti_interp_events) { int *count_addr; oop obj; // Check to see if a field modification watch has been set // before we take the time to call into the VM. count_addr = (int *)JvmtiExport::get_field_modification_count_addr(); if ( *count_addr > 0 ) { if ((Bytecodes::Code)opcode == Bytecodes::_putstatic) { obj = (oop)NULL; } else { if (cache->is_long() || cache->is_double()) { obj = (oop) STACK_OBJECT(-3); } else { obj = (oop) STACK_OBJECT(-2); } VERIFY_OOP(obj); } CALL_VM(InterpreterRuntime::post_field_modification(THREAD, obj, cache, (jvalue *)STACK_SLOT(-1)), handle_exception); } } #endif /* VM_JVMTI */ // QQQ Need to make this as inlined as possible. Probably need to split all the bytecode cases // out so c++ compiler has a chance for constant prop to fold everything possible away. oop obj; int count; TosState tos_type = cache->flag_state(); count = -1; if (tos_type == ltos || tos_type == dtos) { --count; } if ((Bytecodes::Code)opcode == Bytecodes::_putstatic) { Klass* k = cache->f1_as_klass(); obj = k->java_mirror(); } else { --count; obj = (oop) STACK_OBJECT(count); CHECK_NULL(obj); } // // Now store the result // int field_offset = cache->f2_as_index(); if (cache->is_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) { obj->release_byte_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == ltos) { obj->release_long_field_put(field_offset, STACK_LONG(-1)); } else if (tos_type == ctos) { obj->release_char_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == stos) { obj->release_short_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == ftos) { obj->release_float_field_put(field_offset, STACK_FLOAT(-1)); } else { obj->release_double_field_put(field_offset, STACK_DOUBLE(-1)); } OrderAccess::storeload(); } else { if (tos_type == itos) { obj->int_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == atos) { VERIFY_OOP(STACK_OBJECT(-1)); obj->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) { obj->byte_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == ltos) { obj->long_field_put(field_offset, STACK_LONG(-1)); } else if (tos_type == ctos) { obj->char_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == stos) { obj->short_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == ftos) { obj->float_field_put(field_offset, STACK_FLOAT(-1)); } else { obj->double_field_put(field_offset, STACK_DOUBLE(-1)); } } ...//省略不少代码
其余代码不用管,直接看 cache->is_volatile()这段代码,cache是 stop在常量池缓存中的一个实例,这段代码是判断这个cache是不是被 volatile修饰, is_volatile()方法的定义在 accessFlags.hpp文件中,代码以下
public: // Java access flags ...// 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; }
is_volatile是判断是否有 ACC_VOLATILE这个flag,很显然,经过 volatile修饰的stop的字节码中是存在这个flag的,因此 is_volatile()返回true
接着,根据当前字段的类型来给 stop赋值,执行 release_byte_field_put方法赋值,这个方法的实如今 oop.inline.hpp中
inline void oopDesc::release_byte_field_put(int offset, jbyte contents) { OrderAccess::release_store(byte_field_addr(offset), contents); }
赋值的动做被包装了一层,看看 OrderAccess::release_store作了什么事情呢?这个方法的定义在 orderAccess.hpp中,具体的实现,根据不一样的操做系统和CPU架构,调用不一样的实现
以 orderAccess_linux_x86.inline.hpp为例,找到 OrderAccess::release_store的实现,代码以下
inline void OrderAccess::release_store(volatile jbyte* p, jbyte v) { *p = v; }
能够看到其实Java的volatile操做,在JVM实现层面第一步是给予了C++的原语实现。c/c++中的volatile关键字,用来修饰变量,一般用于语言级别的 memory barrier。被volatile声明的变量表示随时可能发生变化,每次使用时,都必须从变量i对应的内存地址读取,编译器对操做该变量的代码再也不进行优化
赋值操做完成之后,若是你们仔细看了前面putstatic的代码,就会发现还会执行一个 OrderAccess::storeload();的代码,这个代码的实现是在 orderAccess_linux_x86.inline.hpp,它其实就是一个storeload内存屏障,JVM层面的四种内存屏障的定义以及实现
inline void OrderAccess::loadload() { acquire(); } inline void OrderAccess::storestore() { release(); } inline void OrderAccess::loadstore() { acquire(); } inline void OrderAccess::storeload() { fence(); }
当调用 storeload屏障时,它会调用fence()方法
inline void OrderAccess::fence() { if (os::is_MP()) { //返回是否多处理器,若是是多处理器才有必要增长内存屏障 // always use locked addl since mfence is sometimes expensive #ifdef AMD64 //__asm__ volatile 嵌入汇编指令 //lock 汇编指令,lock指令会锁住操做的缓存行,也就是缓存锁的实现 __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory"); #else __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory"); #endif } }
os::is_MP()判断是不是多核,若是是单核,那么就不存在内存不可见或者乱序的问题 volatile:禁止编译器对代码进行某些优化.
Lock :汇编指令,lock指令会锁住操做的缓存行(cacheline), 通常用于read-Modify-write的操做;用来保证后续的操做是原子的
cc表明的是寄存器,memory表明是内存;这边同时用了”cc”和”memory”,来通知编译器内存或者寄存器内的内容已经发生了修改,要从新生成加载指令(不能够从缓存寄存器中取)
这边的read/write请求不能越过lock指令进行重排,那么全部带有lock prefix指令(lock ,xchgl等)都会构成一个自然的x86 Mfence(读写屏障),这里用lock指令做为内存屏障,而后利用asm volatile("" ::: "cc,memory")做为编译器屏障. 这里并无使用x86的内存屏障指令(mfence,lfence,sfence),应该是跟x86的架构有关系,x86处理器是强一致内存模型
storeload屏障是固定调用的方法?为何要固定调用呢?
缘由是:避免volatile写与后面可能有的volatile读/写操做重排序。由于编译器经常没法准确判断在一个volatile写的后面是否须要插入一个StoreLoad屏障。为了保证能正确实现volatile的内存语义,JMM在采起了保守策略:在每一个volatile写的后面,或者在每一个volatile读的前面插入一个StoreLoad屏障。由于volatile写-读内存语义的常见使用模式是:一个写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写以后插入StoreLoad屏障将带来可观的执行效率的提高。从这里能够看到JMM在实现上的一个特色:首先确保正确性,而后再去追求执行效率
综上分析能够得知,volatile是经过防止指令重排序来实现多线程对于共享内存的可见性。