Volatile,实现有序性和可见性,是实现线程安全的一种重要机制,是java虚拟机提供的最轻量级的同步机制;能保证线程获取该变量的最新值,避免出现数据脏读现象;其中重要实现机制:防止指令重排序 + 变量操作的可见性;
1、Volatile的案例:
实现线程修改数据的可读;
package online.morn.study.math;
import java.util.concurrent.ArrayBlockingQueue;
public class TestMain extends Thread{
private static boolean flag = false;
//private volatile static boolean flag = false;
/*****
* 特别注意:
* ①System.out.println切记不要使用,因为这里使用了synchronized (this) {} 会进行一次同步
*/
@Override
public void run() {
while (!flag){
// System.out.println("我还在线程中" + flag);
}
// System.out.println("我跳出了线程" + flag);
}
public static void main(String[] args) throws Exception {
new TestMain().start();
Thread.sleep(1000);
flag = true;
// System.out.println("修改值flag =" + flag);
// Thread.sleep(2000);
}
}
2、可见性是什么?
可见性问题是什么首先需要了解清楚可见性问题产生的原因是什么;可参考之前文章 并发问题的产生与规避 ;简要概述就是因为磁盘-缓存-cpu缓存中间存在巨大的执行效率差异,因此在硬件层面增加了cpu告诉缓存提前加载数据,最大化cpu资源,避免cpu等待数据加载;但是在多线程环境下,就存在多个线程的缓存区间之间数据的可见性问题(一个线程进行了操作,另一个线程是否可以读取到最新数据);
可见性的硬件解决方案:
方案1:总线锁,将并行的变为串行;
缺点:性能下降,无法发挥多线程的高性能优势;
方案2:缓存锁,MESI协议, MESI表达的缓存的四种状态(
S-shared 数据有效,存在很多cpu缓存、
E-Exlusive 数据有效,本地缓存独有
M-Modify修改状态 数据有效,但是被修改了,与主内存数据不一致,数只存在本cache中
I-Invalid这行数据无效);
但是其没有解决缓存可见性;
会缓存间的通信与同步时的阻塞{ 当缓存1在进行write的时候,会给缓存2发送失效(invalidate)信息,
缺点:在缓存2失效确认ack发送回来之前缓存1都处于阻塞状态};
方案3:StoreBuffer 引入解决阻塞等待: 当write的时候,将消息发送给 StoreBuffer而不用等待;同步数据的问题交给StoreBuffer
缺点:1、还是存在短暂的数据可见性问题
2、先读取storeBuffer再读主内存的方式,会因为指令重排序的优化造成可见性问题;总结:cpu无法彻底解决在跨线程之间,程序对象之间的依赖关系问题;因此提供了指令:内存屏障;解决数据可见性和指令重排序问题;
可见性的JMM层面解决方案:通过合理的静止缓存中的重排序实现了可见性和有序性;
JMM本质是规范,没有真正的代码实现;
【JMM规范】:多线程环境抽象为: 主内存 -- 多个工作内存 -- 多个线程 ;
解决方案: volatile synchronized final -不可变的 happens-before;
实现理论:
编译器JVM级别的内存屏障 : OrderAccess::loadload() 两个读的屏障 load1一定早于load2
OrderAccess::storeload() 类似全屏障 OrderAccess::loadstore() OrderAccess::storestore()
cpu层面内存屏障 :loadMemoryBarrier 、 storeMemoryBarrier fullMemoryBarrier ;JMM内存屏障:LoadLoadBarriers 、 StoreStoreBarriers、 LoadStoreBarriers 、 StoreLoadBarriers
happens-before规则: 当 a happens before b 存在这种关系的时候则一定对内存可见;而无需使用 volatile ;
JVM中那些操作会建立happens before规则?
规则1,程序顺序规则,程序中的顺序性不可变;
1 happens - before 2
if(a){ // 1
b = 4; //2
}
规则2,volation变量规则,对volation修饰的写一定在读之前;
规则3,传递性规则, 如果 1 happens - before 2 并且 2 happens - before 3 一定会 1 happens - before 3
规则4,线程启动规则() 案例:StartRule.java 在satrt()之前的值修改一定对线程内操作可见;
规则5,线程终止规则 案例:JoinRule.java 在线程中的操作对thread.join之后的线程可见;
规则6,管程锁定规则(synchronized) 案例SynRule.java ThreadA 先拿到锁一定 其操作一定对 后拿到锁的ThreadB可见;
规则7,线程终止规则 对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生
规则8,对象终结规则 构造函数执行的结束一定 happens-before它的 finalize()方法回收
遵循happens-before的案例:
public class VolatileExample {
private int a = 0;
private volatile boolean flag = false;
public void writer(){
a = 1; //1
flag = true; //2
}
public void reader(){
if(flag){ //3
int i = a; //4
}
}
}
红色线为遵循volatile原则的执行顺序: 执行2 先于执行3
蓝色线为遵循传递性规则的执行顺序: 执行1先于执行2 + 执行3 先于执行4 = 执行1 先于执行4,
3、volatile的实现原理:
被volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令;
Lock指令作用:
将当前处理器缓存行的数据写回系统内存;
这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效;
Lock机制:
当对volatile修饰的变量进行写操作,JVM会向处理器发送一条Lock前缀的指令, 将所在缓存行的数据写回系统内存。
而其他处理器的缓存值则通过嗅探在总线上传播的数据检查自己缓存的值有没有过期;如果失效则重新从系统内存中读取处理器主缓存中的数据。
实现核心:
①happen-before原则:
②volatile内存语义:
当本地内存发生写writer操作的时候,会将数据同步写入到主内存,并给线程B发送一个数据失效的信息通信,线程B就会自动去主内存获取了;
内存语义的实现: JMM内存屏障
volatile重排序规则表:
volatile采取的策略:
- 在每个volatile写操作的前面插入一个StoreStore屏障;
- 在每个volatile写操作的后面插入一个StoreLoad屏障;
- 在每个volatile读操作的后面插入一个LoadLoad屏障;
- 在每个volatile读操作的后面插入一个LoadStore屏障。
需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障
StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;
StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序
LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序
LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序