1、volatile关键字的作用就是解决了线程间的可见性问题。
2、什么是线程可见性问题?
在单线程的环境下,如果向一个变量先写入一个值,然后在没有写干涉的情况下读取这个变量的值,那这个时候读取到的这个变量的值应该是之前写入的那个值。这本来是一个很正常的事情。但是在多线程 环境下,读和写发生在不同的线程中的时候,可能会出现:读线程不能及时的读取到其他线程写入的最 新的值。这就是所谓的可见性。
3、volatile关键字的基本用法
public class VolatileDemo1 implements Runnable{
public volatile static boolean stop = false;
@Override
public void run() {
int i = 0;
while (!stop){
i++;
/*
方式1:释放锁操 会强制工作内存的写操作的数据刷新到主内存。
System.out.println("2132"); 由于输出的方法里面存在释放锁操作。
synchronized (this){
}
*/
//System.out.println("2132");
//synchronized (this){
//}
/*
方式2:IO操作 会强制工作内存的写操作的数据刷新到主内存。
new File("c:/test.txt");
*/
//new File("c:/test.txt");
/*
方式3:线程沉睡 会强制工作内存的写操作的数据刷新到主内存。
Thread.sleep(long);
*/
//Thread.sleep(0);
}
System.out.println("rs:" + i);
}
public static void main(String[] args) throws InterruptedException {
new Thread(new VolatileDemo1(),"t1").start();
Thread.sleep(1000);
stop=true;
/*
当stop变量不使用volatile 修饰的时候,主线程修改了其值以后,
对于t1线程来说是不可见的,所以会存在可见性问题。
当我们使用volatile来修饰stop变量变量的时候,一旦主线程修改了
stop变量的值,对于他线程来说就是可见的。
除了使用volatile来修饰stop 变量,我们还有其他方式,我们可以执行synchronized操作,因为
锁释放的操作会强制将工作内存的写操作的数据刷新到主内存。
除了释放锁操作可以刷新主内存之外,还有IO操作也会强制刷新主内存,如 new File("c:/test.txt")。
除此之外 Thread.sleep 也会强制将工作内存的写操作的数据刷新到主内存。
*/
}
}
4、volatile加上以后对Java代码编译成汇编指令后有啥区别?
我们使用hsdis工具来查看Java 代码通过编译后的汇编指令。
工具用法:1、将如下图中的两个文件添加到JRE_HOME/bin/server路径下。
2、在运行main函数之前,加入如下虚拟机参数:
-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
-XX:CompileCommand=compileonly,*VolatileDemo1.getInstance(替换成实际运行的代码)
我们使用工具可发现,使用volatile关键字之后,Java代码编译成的汇编多了一个Lock指令。
0x00000000037028f3: lock add dword ptr [rsp],0h ;*putstatic stop
5、我们上面说了加上volatile关键字在代码编译后的汇编指令会多一个lock的指令,这个指令牵扯到cpu缓存锁的概念,下面我们来探究一下
在计算机中cpu层面有缓存的概念,叫做cpu的高速缓存,如下视图:
cpu在执行指令的时候,会先去高速缓存中查找,比如定义一个变量static boolean stop = false,如果cpu执行的线程需要使用此变量,会先去cpu的缓存中找,如果没有就去主内存中找,找到后就会放到cpu缓存中,方便下一次获取使用,读写都是操作cpu的缓存。如果多个线程由多个cpu去执行的时候都使用到了stop变量,那么就会有多个cpu缓存了stop变量,这个时候如果有线程修改了stop变量(修改的是当前cpu中的缓存,会不确定时间同步主内存。),且还没有同步到主内存,那么其他线程使用stop变量的时候就出现了值不是最新的,这也就是可见性问题产生的原因。这个时候就引出了cpu缓存一致性的问题。毫无疑问为了处理多cpu的缓存一致性问题,操作系统的开发人员允许使用加锁来解决cpu缓存一致性的问题,如上图所示cpu与内存交互是通过总线的,那么cpu就提供了一个 总线锁,总线锁会阻塞其他cpu,开销比较大,因此操作系统开发人员由提供了 缓存锁,也就是收保护的资源是cpu的高速缓存。前面说的lock指令就跟这两个锁有关系。
那什么时候使用缓存锁呢?
1、cpu架构支持缓存锁。
2、当前数据是否存在于缓存。
上面抛出了多cpu缓存一致性问题,在解决这个问题的方案中由很多的缓存一致性协议如msi、mesi、mosi等协议。
我们主要说明一下mesi协议,mesi表示4钟缓存状态,如下:
1. M(Modify) 表示共享数据只缓存在当前CPU缓存中,并且是被修改状态,也就是缓存的数据和主内存中的数据不致。
2. E(Exclusive) 表示缓存的独占状态,数据只缓存在当前CPU缓存中,并且没有被修改
3. S(Shared) 表示数据可能被多个CPU缓存,并且各个缓存中的数据和主内存数据一致
4. I(Invalid) 表示缓存已经失效
这种一致性协议的作用如下图:
早期cpu缓存过期通知策略:
这种方式会造成cpu阻塞,开销较大,为了解决这个问题,操作系统的开发人员引入了Store Bufferes的概念,如下图:
引入Store Bufferes概念后会导致一个严重的问题产生,那就是指令重排序,比如下代码运行结果可能会出现错误。
executeToCPU0(){
a=1;
b=1;
}
executeToCPU1(){
while(b==1){
assert(a==1);
}
}
假设分别有两个线程,分别执行executeToCPU0和executeToCPU1,分别 由两个不同的CPU来执行。 引入Store Bufferes之后,就可能出现 b==1返回true ,但是assert(a==1)返回false。原因是cpu0在执行a=1的时候,会先将指令放入到Store Bufferes中,然后异步通知cpu1先失效,而此时cpu0可能会出现先将b=1的指令执行完成了才正在的从Store Bufferes中获取
到a=1的指令执行,这就是Store Bufferes引起cpu指令重排序的问题。 如下流程:
为了解决指令重排序问题,操作系统的开发人员引入了 内存屏障 的模型。
通过内存屏障禁止指令重排:
X86的memory barrier指令包括lfence(读屏障) sfence(写屏障) mfence(全屏障)。
1、Store Memory Barrier(写屏障):告诉处理器在写屏障之前的所有已经存储在存储缓存(store bufferes)中的
数据同步到主内存,简单来说就是使得写屏障之前的指令的结果对屏障之后
的读或者写是可见的,可理解为立马同步主内存
2、Load Memory Barrier(读屏障):处理器在读屏障之后的读操作,都在读屏障之后执行。配合写屏障,使得写屏
障之前的内存更新对于读屏障之后的读操作是可见的,可理解为直接读取主内存。
3、Full Memory Barrier(全屏障):确保屏障前的内存读写操作的结果提交到内存之后,再执行屏障后的读写操作。
伪代码如下:
volatile int a=0; //volatile关键字添加的lock指令就是在指令的前后加上了读写的内存屏障来保证线程的可见性。
executeToCpu0(){
a=1;
storeMemoryBarrier() //写屏障,会立马将a=1写入到内存,
//屏障后的读取都是最新的,插入这个
//屏障也就表明a=1; b=1;两个指令不允许指令重排序。
b=1;
}
executeToCpu1(){
loadMemoryBarrier(); //读屏障,可理解为屏障后的指令,直接去住内存读取数据。
assert(a==1) //true
}
不同的cpu架构、os 架构的内存屏障指令可能不同,因为Java 是一次编写,到处运行,所以Java中就提供了一个Java内存模型(JMM)来处理因为平台差异化内存访问方式不同的问题。JMM本身是一种抽象的概念,并不真实存在,它定义了程序中各个共享变量的访问规则,即在虚拟机中将变量存储到内存和从内存读取变量这样的底层细节。JMM 屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能使用Java提供的高级指令(如volatile)来达到一致的内存访问效果。
6、有了上面内存屏障的知识点,我们回归volatile关键字的原理
我们查看Java编译后的class字节码发现volatile修饰的变量会多一个ACC_VOLATILE 的class指令(注意区别于汇编的lock)。
public static volatile boolean stop;
descriptor: Z
flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE
这个class 指令就是在jvm对stop 指令进行读写的时候,会判断变量是否有ACC_VOLATILE指令,如果有就会在赋值后加上内存屏障的操作。
如下jvm源码(bytecodeInterpreter.cpp):
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));
}
}
代码中的OrderAccess::storeload();表示添加写屏障,我们查看其jvm源码如下:
OrderAccess::storeload();这个操作在jvm中就有不同os架构下的是实现,如下:
我们挑选x86的实现看其源码如下:
inline void OrderAccess::loadload() { acquire(); }
inline void OrderAccess::storestore() { release(); }
inline void OrderAccess::loadstore() { acquire(); }
inline void OrderAccess::storeload() { fence(); }
inline void OrderAccess::acquire() {
volatile intptr_t local_dummy;
#ifdef AMD64
__asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) : : "memory");
#else
__asm__ volatile ("movl 0(%%esp),%0" : "=r" (local_dummy) : : "memory");
#endif // AMD64
}
inline void OrderAccess::release() {
// Avoid hitting the same cache-line from
// different threads.
volatile jint local_dummy = 0;
}
inline void OrderAccess::fence() {
if (os::is_MP()) {
// 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
}
}
inline void OrderAccess::storeload() { fence(); } 方法的部分实现如下:
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory"); 看见没有lock指令,这就是之前说的Java代码编译成的汇编volatile关键字多出来的lock指令,volatile 修饰的变量,就是在读写的时候,jvm里面自动为其加上内存屏障来解决可见性。
7、happens-before 模型
除了显示引用volatile关键字能够保证可见性以外,在Java中,还有很多的可见性保障的规则。
7.1、程序顺序规则(as-if-serial语义):
规定1、不能改变程序的执行结果(在单线程环境下,执行的结果不变)。
规定2、依赖问题, 如果两个指令存在依赖关系,是不允许重排序。
案例:
int a=0;
int b=0;
void test(){
int a=1; a
int b=1; b
//允许a、b赋值重排序。
//int b=1;
//int a=1;
int c=a*b; c
}
a happens -before b ; b happens before c b happens before c 的意思就是b对于c出的操作是可见的。
7.2、传递性规则 :
如果 a happens-before b , b happens- before c, 那么 a happens-before c
7.3、volatile变量规则:
volatile 修饰的变量的写操作,一定happens-before后续对于volatile变量的读操作.,其实现为内存屏障机制来防止指令 重排。
案例 :
public class VolatileExample{ int a=0; volatile boolean flag=false; public void writer(){ a=1; 1 flag=true; //修改 2 } public void reader(){ if(flag){ //true 3 int i=a;//1 4 } } }
1 happens-before 2 是否成立? 是
3 happens-before 4 是否成立? 是
2 happens -before 3 ->volatile规则
1 happens-before 4 ; i=1成立。
volatile规则里面有一个规则如下表:
根据这个表可知道案例中 1 happens-before 2
7.4、监视器锁规则:
案例:
int x=10; synchronized(this){ //后续线程读取到的x的值一定12 if(x<12){ x=12; } } 其他线程在此处读取到的值一定是x=12 x=12;
7.5、start规则:
案例:
public class StartDemo{ int x=0; Thread t1=new Thread(()->{ //读取x的值 一定是20 if(x==20){ } }); //启动线程之前先设置为x=20, 那么在t1里获取的x一定等于20. x=20; t1.start(); }
7.6、join规则:
案例:
public class Test{
int x=0;
Thread t1=new Thread(()->{
x=200;
});
t1.start();
t1.join(); //保证结果的可见性。
在此处读取到的x的值一定是200。
}
7.7、final关键字也提供了内存屏障的规则。