最近项目中用到并发和多线程较多,之前都有所了解,但没有完整的学习,趁此次机会把 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 呢,下一次再来写这个问题。