最近项目中用到并发和多线程较多,之前都有所了解,但没有完整的学习,趁此次机会把 java.util.concurrent 包完整的总结学习一遍。
研究juc包,少不了要先研究一下volatile 关键字 和 cas 算法,因为在juc包中很多地方都使用到了volatile 和 cas ,今天先来写volatile。
volatile 关键字之前也或多或少的接触过,之前只了解是为了解决多线程之间的内存可见性问题,只是使用,并没有追究其中的原理。
假如如果我们有这样一个需求,程序有两个线程,两个线程共享一个开关参数,当线程A将开关关闭后,则线程B结束执行,否则线程B一直运行。
编写测试程序如下:
public class VolatileTest {
//这里main方法代表线程B
public static void main(String args[]) {
ThreadA threadA = new ThreadA();
new Thread(threadA).start();
while (true) {
if(!threadA.isFlag()) {
System.out.println("开关已关闭,退出...");
break;
}
}
}
}
class ThreadA implements Runnable {
private boolean flag = true;
@Override
public void run() {
try {
Thread.sleep(2000); //睡两秒钟
} catch (InterruptedException e) {
}
flag = false;
System.out.println("线程A在执行...");
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
我们理想的运行结果应该是:
线程A在执行...
开关已关闭,退出...
但实际运行结果确是:
线程A在执行...
然后主线程一直在while 循环,
按照正常思路,我们已经在线程B
(Main线程)
循环执行过程中通过线程A中修改了flag的值为false,但线程B(Main线程)一直循环却没有达到退出条件,也就是线程B 获取
threadA.isFlag()的值始终是true,是不是比较奇怪,但为什么会这样的,我们看一下其中的缘由。
程序在执行时,所有的指令都是在CPU中执行的,包含数据的读取和写入,由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
结合以上,我们再来看一下我们的程序,首先在主内存中声明了 flag = true,此时线程B(Main线程)从主内存中获取flag=true的值放入缓存中,线程A从主内存中获取flag=true 放入缓存中,接着修改flag=flase,更新主内存的值 flag = false,但线程B中已经缓存了flag=true,没有再去同步主内存的值,所以就导致了我们常说的内存可见性问题。
那我们结合一下之前了解到的知识来解决一下这个问题:
while (true) {
synchronized (threadA) {
if(!threadA.isFlag()) {
System.out.println("开关已关闭,退出...");
break;
}
}
}
在主线程执行时对线程A 添加 synchronized,此时执行结果与期望值一致(Lock也可实现):
线程A在执行...
开关已关闭,退出...
但了解synchronized的应该都知道,synchronized是互斥锁,也就是线程B必须要等待线程A执行完成才能继续往下执行,为了解决内存可见性问题,java提供了 volatile 关键字来保证可见性,我们修改一下程序再来看一下执行结果,修改如下:
private volatile boolean flag = true;
将falg 声明为 volatile 变量,同时删除上一步添加的同步锁(synchronized),执行结果如下:
线程A在执行...
开关已关闭,退出...
但是为什么将变量使用volatile 来修饰就可以保证内存的可见性呢,主要有以下几点:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程A进行修改时,会导致线程B的工作内存中缓存变量flag的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程B的工作内存中缓存变量flag的缓存行无效,所以线程B再次读取变量flag的值时会去主存读取。
那volatile 是不是可以替代 synchronized 呢,下一次再来写这个问题。