1. volatile语义解决的问题:
(1)可见性的保证:如果一个线程线程对共享变量进行修改,能够实现本地的内存的立即回写到主内存,通过嗅探机制,其他线程能够立即感知到最新的变化。
(2)顺序性的保证:禁止JVM或者CPU对进行指令重排。
可见性保证举例:
//线程A
boolean running= true;
public void run(){
while(running){
System.out.println(“hello!”);
}
}
//线程B
public void setRunStatus(boolean running){
this. running = running;
}
上面的代码,如果调用setRunStatus,把running更改为false,在单线程的环境中是能够正常中断的,但是如果在多线程的环境里面,上面代码或许可以中断,但是也有可能会导致无法中断线程,从而引起死循环。这是因为每个线程运行的时候都有自己的工作内存,那么线程A在运行的时候,会将running变量的值拷贝一份放在自己的工作内存当中。如果线程B修改改了stop变量的值之后,但是还没来得及写入主存当中,线程A被挂起(CPU的调度机制是时间片轮转机制),那么线程A由于不知道线程B对running变量的修改,还会一直循环下去。
可以使用running前面加上volatile来解决该问题:
(1)使用volatile关键字之后,变量如果被修改会被强制将修改的值立即写入主存;
(2)使用volatile关键字之后,当线程B进行修改时,会导致线程A的工作内存中缓存变量running的缓存行失效;
(3) 由于线程A的工作内存中缓存变量running的缓存行失效,所以线程A再次读取变量running的值时会去主存中读取。当在线程B修改running的值时,也会让线程A的工作内存中的变量running的缓存行无效,然后线程A读取该变量时,发现缓存行无效,它会去对应的主存读取最新的值,然后更新到自己的工作内存。那么线程A读取到的就是最新的正确的值。
顺序性保证举例:
为了提高性能,JVM编译优化和CPU 都可能对程序进行指令重排,只要重排的指令语义保持一致。指令重排序可能会给我们的程序执行带来不确定性,比如:
public class OrderExample{
private boolean running= false;
private String property;
public void init() {
property = getProperty();//1
running = true;//2
}
public void work() {
if (running) {//3
System.out.println(property+” ok”);//4
}
}
public String getProperty(){
...
}
}
running变量是为了作为是否执行的判断条件。其中1,2操作是没有数据依赖关系,同样3、4操作也是没有数据依赖关系。那么CPU可能对1、2和3、4操作都可能进行重排序。现在开启线程A操作init()方法,线程B操作work ()方法,重排序将会对多线程产生影响。
2操作有可能被排序在1操作前面执行。当CPU时间片转到线程B。线程B判断 if (running)为中的running是否为true,接下来接着执行操作4,但property可能还没有初始化,从而导致4发生异常。为了防止重排序引发的问题。Java内存模型规定了使用volatile来修饰相应变量时,可以让CPU在处理指令的时候禁止重排序。
public class OrderExample {
private volatile boolean running = false;
private String property;
public void init() {
property = getProperty();//1
running = true;//2
}
public void doSomething() {
if (running) {//3
System.out.println(property+” ok”);//4
}
}
public String getProperty(){
...
}
}
java内存模型Happen-before 机制与volatile的约束:
Java的volatile 关键字在可见性的基础上提供了 happens-before担保机制。happens-before 机制确保了 volatile 的完全可见性:
(1)、如果其他变量的读写操作原本发生在 volatile 变量写操作之前,他们不能被指令重排到 volatile 变量的写操作之后。发生在 volatile 变量写操作之后的读写操作仍然可以被指令重排到 volatile 变量写操作之前。happen-after 重排到 (volatile写) 之前是允许的,但 happen-before 重排到之后是不允许的。
(2)、如果其他变量的读写操作原本发生在 volatile 变量读操作之后,他们不能被指令重排到 volatile 变量的读操作之前。发生在 volatile 变量读操作之前的读操作仍然可以被指令重排到 volatile 变量读操作之后。happen-before 重排到 (volatil读) 之后是允许的,但 happen-after 重排到之前是不允许的。
volatile不能保证原子性:
i = 5; //1
i++; //2
i= i + 1; //3
以上三条操作中,只有1操作是原子操作,这是因为:
1操作是将数值1赋值给i,也就是说线程执行这个语句的会直接将数值5写入到线程的工作内存中。
2操作实际上包含2个操作,它先要去读取i的值,然后将i的值写入工作内存,虽然读取i的值和将i的值写入工作内存都是原子性操作,但是两个操作合并起来就是不是原子操作。
同样的,i++和 i = i+1包括3个操作:先读取i的值,然后进行加1操作,写入新的值。
2、volatile的实现机制
在Java中对于volatile修饰的变量,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序问题
编译器在指令序列插入的内存屏障保守插入机制如下:
在每个volatile写操作的前面插入一个storestore屏障。
在每个volatile写操作的后面插入一个storeload屏障。
在每个volatile读操作的后面插入一个loadload屏障。
在每个volatile读操作的后面插入一个loadstore屏障
· storestore屏障:对于语句store1; storestore; store2,在store2及后续写入操作执行前,保证store1的写入操作对其它处理器可见。(也就是说如果出现storestore屏障,那么store1指令一定会在store2之前执行,CPU不会store1与store2进行重排序)
· storeload屏障:对于语句store1; storeload; load2,在load2及后续所有读取操作执行前,保证store1的写入对所有处理器可见。(也就是说如果出现storeload屏障,那么store1指令一定会在load2之前执行,CPU不会对store1与load2进行重排序)
· loadload屏障:对于语句load1; loadload; load2,在load2及后续读取操作要读取的数据被访问前,保证load1要读取的数据被读取完毕。(也就是说,如果出现loadload屏障,那么load1指令一定会在load2之前执行,CPU不会对load1与load2进行重排序)
· loadstore屏障:对于语句load1; loadstore; store2,在store2及后续写入操作被执行前,保证load1要读取的数据被读取完毕。(也就是说,如果出现loadstore屏障,那么load1指令一定会在store2之前执行,CPU不会对load1与store2进行重排序)