(一)JUC 之 volatile 关键字

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

猜你喜欢

转载自jifeng3321.iteye.com/blog/2394699