上 并发编程一:深入理解JMM和并发三大特性( 二 )


【上并发编程一:深入理解JMM和并发三大特性】内存交互操作
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、 如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:
Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
可见性深入分析
下面通过一个例子对可见性进行深入分析
package demo;public class visibilityDemo {privateboolean flag = true;private int count = 0;public void refresh() {flag = false;System.out.println(Thread.currentThread().getName() + "修改flag:"+flag);}public void load() {System.out.println(Thread.currentThread().getName() + "开始执行.....");while (flag) {//TODO业务逻辑count++;}System.out.println(Thread.currentThread().getName() + "跳出循环: count=" + count);}public static void main(String[] args) throws InterruptedException {visibilityDemo test = new visibilityDemo();// 线程threadA模拟数据加载场景Thread threadA = new Thread(() -> test.load(), "threadA");threadA.start();// 让threadA执行一会儿Thread.sleep(1000);// 线程threadB通过flag控制threadA的执行时间Thread threadB = new Thread(() -> test.refresh(), "threadB");threadB.start();}}
解释一下,load方法是一个while循环,依赖于flag这个变量,方法修改flag变量值 。执行main方法,首先flag是true,因此执行load方法是一个死循环,修改flag为false,能不能结束这个循环呢?看一下执行结果
并没有跳出循环 。为什么没有跳出循环呢?这里涉及到并发编程的可见性 。就是说B线程修改了flag的值,但是这个值对于A线程来说不可见 。那为什么不可见,我们来了解一下线程的内存数据是如何调用的 。
在看下这个图,假设线程1在执行while方法,首先会先从自己的工作内存当中加载flag,但是第一次加载的时候工作内存中没有flag这个值,那么就会从主内存中通过read、load获取到flag=true到工作内存中,线程1通过use来使用这个flag 。此时线程1在执行死循环 。
假设此时线程三修改这个flag的值,同样第一次线程三的工作内存中没有flag,就会从主内存中通过reda、load加载到工作内存中,通过对flag赋值为false,然后通过store、write写入到主内存中 。此时主内存中的flag=false 。
但是线程一中的工作内存有这个flag的值为true,所以不会重新从主内存获取flag的值 。因此线程三对flag的修改对于线程一来说是不可见的 。
看了上述的过程就会产生一系列的问题,比如
在解释这些问题之前,先来看下如何修改这些代码能够跳出循环,再根据这些代码一一解释 。
方案1:在flag 加上即flag = true; 改为flag = true;
结果:跳出循环
方案2:在 count 加上
结果:跳出循环
方案3:使用内存屏障
结果:跳出循环
方案4:使用.yield()
结果:跳出循环
方案5:使用.out.()
结果:跳出循环
方案6 使用int count 类型换成
方案7:使用凭证
等等还有很多方案,以上方案都是能够满足可见性的 。
下面具体一个个方案来解释:首先要从java层面是很难去解释,要从JMM角度去解释,JMM角度上面有解释过了也就是如何让工作内存重新从主内存加载flag这个值 。在解析这些方案之前先回答下上面提出的几个问题
什么时候会刷新主内存的值,是一修改就刷新还是线程结束后在刷新?
可以这么理解,如果不做任何额外操作的话,在线程中修改变量的话是不会马上刷新主内存的值的,当这个线程对这个变量不使用的情况下,那么这个变量就会回收,在回收前就会刷新主内存的值 。