一、volatile关键字具有可见性
在以下例子中,可以发现threadA线程无法感知到flag的变化,导致“线程threadA感知到flag的改变”这行代码一直无法在控制台打印出来。但是,如果使用volatile来修饰flag后发现threadA可以感应到flag的变化,但是底层又是怎么实现的呢?
public class TestVisibility {
private static boolean flag = false;
public static void main(String[] args){
Thread threadA = new Thread(()->{
while (!flag){}
System.out.println("线程:" + Thread.currentThread().getName()
+ "感知到flag的改变");
},"threadA");
threadA.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread threadB = new Thread(()->{
System.out.println("start.......");
flag = true;
System.out.println("end.......");
},"threadB");
threadB.start();
}
}
CPU的工作模式:内存中的数据 -> 生成数据副本(不同的线程生成各自的数据副本,互不影响) -> bus总线 -> 缓存
从流程图来看,threadA和threadB对flag的操作是相互隔离的,这就是为什么threadA线程无法感知到flag的变化。但是当threadB线程将flag的取值写回到内存后,threadA还是有可能感知到flag的变化的(怎么感知?答:由于CPU缓存的最小单位是缓存行,一般是64KB,当flag所在的缓存行中有数据过期需要重新从内存中获取数据的时候,就会将整个缓存行失效,这样也就可以读取到最新的flag取值了。只不过这种情况就有一定的随机性和延迟性了)。
threadA如何能够立刻感知到flag的变化呢?
1>总线锁
2>MESI缓存一致性协议
1>总线锁
2>MESI缓存一致性协议(添加volatile来修饰flag)
第一步:当threadA第一次去查询flag的时候,会将副本信息通过总线传递给CPU,此时数据状态为E(独享);
第二步:当threadB去查询flag的时候,发现threadA在使用该字段,则生成的副本信息中数据状态为S(共享),并将threadA中的数据状态改为S;
第三步:当threadA和threadB同时对flag进行操作的时候,总线会通过总线裁决来决定哪个线程先执行。假如threadB获取了执行权,它就会和总线要数据了,将它对应的数据副本加载到CPU缓存中。当threadB要进行数据修改的时候会将数据状态由S改为M(修改)并会通过总线通知threadA将数据状态由S改为I(失效);
第四步:threadB会将修改后的数据存到CPU的StoreBuffer中,由于通知其它副本将状态改为I需要经过总线操作,相对耗时。给人的感觉就是一失效,我就可以从StoreBuffer中获取到最新的数据了,就可以将失效状态改为S了;
第五步:StoreBuffer中的数据什么时候会通过总线写会内存,这个时间是不确定的。
二、volatile关键字为什么不具有原子性
例如:counter++
第一步:当threadA第一次去查询counter的时候,会将副本信息通过总线传递给CPU,此时数据状态为E(独享);
第二步:当threadB去查询counter的时候,发现threadA在使用该字段,则生成的副本信息中数据状态为S(共享),并将threadA中的数据状态改为S;
第三步:当threadA和threadB同时对counter进行操作的时候,总线会通过总线裁决来决定哪个线程先执行。假如threadB获取了执行权,它就会和总线要数据了,将它对应的数据副本加载到CPU缓存中。当threadB要进行数据修改的时候会将数据状态由S改为M(修改)并会通过总线通知threadA将数据状态由S改为I(失效);
第四步:threadB会将修改后的数据存到CPU的StoreBuffer中,由于通知其它副本将状态改为I需要经过总线操作,相对耗时。给人的感觉就是一失效,我就可以从StoreBuffer中获取到最新的数据了,就可以将失效状态改为S了;
第五步:StoreBuffer中的数据什么时候会通过总线写会内存,这个时间是不确定的。
threadA中标志为I的命令是怎么处理的呢?
1>直接丢弃,从store buffer获取到最新的值,不需要再写回内存;
2>继续执行完当前指令,此时counter为0,则继续执行counter = counter+1,结果为1,写回内存。这样就相当于threadA和threadB都写回到了主内存中,counter的取值都为1。