java并发之volatile
作用:保证变量的可见性
可见性问题的由来:
由于CPU比内存以及磁盘的速度快得多,CPU操作一个变量需要等到内存、磁盘读取成功。这时的CPU会死等在这里,极大的影响了CPU利用率。为了解决这个问题、硬件工程师先是增加了高速缓存,事先让一些常用变量加载到高速缓存中,这样CPU就不需要等待内存加载成功,这样提高了CPU的利用率,但是体验还是不行。为了提升CPU的利用率,发明了线程,在CPU死等的时候去运行其他的内容,极大地利用了CPU资源。但是工程师没有满足,为了得到更快的运行效率,提出了多核CPU。本来在单核CPU的多线程环境中没有可见性问题,但是随着CPU从单核变为多核就出现了可见性问题。
可见性问题:
有一个场景,有两个CPU核心,每个CPU核心有一个缓存。主内存保存有count=0;有两个线程对count进行循环加一操作。
当第一个线程得到CPU0的时间片时,对count进行加操作。即从0开始加。
而此时第二个线程恰好得到CPU1的时间片,也会进行count的加操作。也会从缓存里面读出值,此时CPU1缓存中的值还是0,并不是CPU0中已经计算到的值。就会进行从0开始加的操作。这个就是可见性问题。
其原因是因为不同CPU的缓存中的数据没有共享。、
解决可见性问题
因为CPU的缓存数据没有共享,才导致可见性问题,解决方法当然是让数据共享,即让不同CPU的缓存一致。可以通过缓存锁的的方式来保证缓存一致。即让CPU读写是遵循缓存一致性协议,比如MESI协议。该协议规定了缓存的四种状态。其中M、S、E状态下CPU可以从缓存中读取数据,当缓存处于I状态,CPU只能从主内存中读取数据。这样就可以保证缓存一致性。大致思路是如果一个CPU修改了值,他会告知其他CPU他缓存中的值现在已经无效了,需要从主内存里面重新加载,其他的CPU在接受到这个信息之后,会返回一个ACK给修改值的CPU。CPU接收到值之后才进行将数据同步到主内存,再接着运行其他指令,当然这个过程是阻塞的。为了解决阻塞问题,提高CPU的利用率,引入了一个称作StoreBuffer的东西,即CPU在通知其他CPU之后,就将该数据保存在StoreBuffer,自己去执行其他代码去了,不阻塞在这里。等到ACK返回之后,再将StoreBuffer中的数据同步到缓存以及主内存中。
重排序问题
但是,通过该方式还有一个问题没有解决,就是指令重排序的问题。上面讲到,为了最大化利用CPU资源,CPU死等其他CPU的ACK,它会先运行其他指令。这样就会打乱一些变量之间的逻辑关系。举个简单的例子。见下面代码。
package com.test.demo;
public class VolatileTest {
static boolean is_Finish =false;
static int i =0;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
i = 10;
is_Finish = true;
});
Thread thread1 = new Thread(() -> {
if (is_Finish) {
if (i == 10) {
System.out.println("true");
}else{
System.out.println("false");
}
}
});
thread.start();
thread1.start();
}
}
上面代码,怎么看就该输出true,但是按照原来的思路来看会有过一个问题!
即当is_Finish请求的ACK比i的ACK要返回的快,该程序是会输出false的。is_Finish已经通过MESI协议了,i还没有通过,此时i还是0;
这就又有了一个新的问题,指令重排序。
为了解决重排序,设置了内存屏障,CPU在处理有内存屏障的数据时就不考虑CPU的利用率问题了,就会阻塞在这里,不去接下去运行其他指令,一定要等待ACK响应才会打开。
JMM是什么?
Java语言时一个跨平台的语言,不同的平台的硬件系统不一样,就导致了设置内存屏障方式的不同,JAVA为了解决这个问题,将对内存以及高速缓存的读写操作进行了抽象,这个抽象的读写过程就是JMM。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0tpneiS9-1619163985688)(C:\Users\15292\AppData\Roaming\Typora\typora-user-images\image-20210423152833258.png)]
这是Java层面的读写操作,我们通过这个读写操作去设置内存屏障,再去调用不同硬件系统的内存屏障的API,就达到了写一个JAVA程序,适应不同的硬件系统。
Java语言设置内存屏障解决可见性以及重排序的方式
volatile、synchronized、final以及happens-before规则
happens-before就是表示一个代码的顺序关系
1 happens-before 2,代表1一定比2先执行。
JMM中建立happens-before规则的方式
程序顺序规则
监视器锁的规则
volatile变量规则
传递性
start()规则
线程终止原则
线程中断规则
join()规则
对象终结规则