Java多线程系列之volatile关键字

    在Java并发编程中synchronized和volatile是使用较为频繁的两个关键字,咱们知道synchronized保证了在同一时刻多线程中只有一个线程能够获取到锁执行同步代码块,而volatile比起synchronized更加轻量级,被volatile修饰的变量具备多线程修改可见性,当多线程访问被volatile修饰的共享变量有一个线程修改了该变量其余线程能够当即察觉到变量修改,获取到修改后的新值。java

    在理解volatile关键字以前咱们须要先从硬件层面了解下CPU、高速缓存和主存的关系。编程

 

1 - CPU、高速缓存和主存

    计算机的硬件组成主要有总线、IO设备、主存和CPU等,数据主要存放在主存中,CPU负责执行指令,CPU中大部分指令执行只须要几个时钟周期,而主存中数据读写一般须要几十甚至上百个时钟周期,因为CPU的指令的执行速度远高于主存中的数据读取,若是从CPU中直接读取主存中的数据进行处理中间会有很长时间的浪费,为了平衡这种差别因而出现了高速缓存。高速缓存其实就是在CPU和主存之间开辟一片空间,在程序指令执行过程当中先将须要操做的主存中的数据复制一份到高速缓存中,CPU执行指令时直接从它的高速缓存中读取数据,在程序结束时将数据会写到主存中,这样就有效下降了由于CPU执行指令和CPU从主存读取数据的时间延迟。可是这样作又会产生新的问题,若是是在单核CPU中不会有问题可是在多核CPU中就会出现新的问题,示意图以下:缓存

    这种硬件架构存在的问题是在多核CPU中当有多个线程去访问主存中的同一个共享变量或数据,Core0和Core1分别会在各自的工做缓存中为该变量建立副本,这时候若是没作额外控制,当其中一个执行线程好比Core0修改了变量值,此时Core1不知道读取的还是本身副本中的数据,就会去操做这个脏数据,这就是这种架构致使的缓冲数据不一致的问题。解决问题的方法有主要有两种:1.由于高速缓存和主存的交互都会通过总线,不管是从主存中复制数据到高速缓存仍是从高速缓存将数据刷新到主存。咱们能够直接对总线加锁,但是这样的话某一时刻就只能有一个线程对共享变量进行读写,实质就是把多核操做变成单核;2.经过缓存锁即缓存一致性协议实现,在Core0操做一个变量时若是发现是共享变量当即发出信号通知其余持有该变量副本的CPU如Core1将缓存中该变量副本设置为失效,并将本身工做缓存中修改的共享变量当即刷新到主存,下次Core1会直接从主存中读取共享变量。多线程

2 - Java内存模型

    Java为了在虚拟机层面为不一样硬件和操做系统解决这种缓存不一致性问题提供一个统一的解决方案。它在JVM中从新定义了一个java内存模型(即JMM),模型设计如图:架构

 

 

    Java内存模型定义了共享变量的访问规则,共享变量存储在主内存中,当多线程并发访问共享变量时会为每一个线程单独分配一片工做内存,从主内存中复制一份共享变量到各自的工做内存中,在通常状况下对共享变量的操做在工做内存中的变量副本上进行在线程执行结束以后将修改后的变量副本刷新到主内存中,线程间不能互相访问对方的工做内存,所以线程方法内部对共享变量修改其余线程是不可见的。使用volatile修饰的共享变量,多线程环境下有线程对它进行修改时会在工做内存中和主存中进行变量的同步更新,其余线程读取被volatile修饰的共享变量时会将工做内存中的变量副本设置为失效直接从主内存中读取该共享变量。此外虚拟机为了提高性能会在编译器和程序执行时进行指令重排序,这在单线程环境下没有问题,但虚拟机进行指令重排序时只考虑存在显式依赖关系变量的操做指令的前后顺序保证最终操做结果与重排序以前的执行结果一致但在多线程环境下仍然可能产生错误的执行结果,例如分析下下面代码:并发

package com.clpublic.factory.test;

public class VolatileTest {
    int x = 0;
    volatile  boolean flag = false;

    public void write() {
        x = 42;
        flag = true;
    }

    public void read() {
        if (flag == true) {
            System.out.println(x);
        }
    }

    public static  void main(String[] args){
        VolatileTest test = new VolatileTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.read();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.write();
            }
        }).start();
    }
}

    若是volatile未对编译器和运行期的指令重排序作出限制,那么程序的执行结果可能就不是42,考虑这样一种状况经指令重排以后read方法执行顺序变成 flag = true->x = 42那么此时write线程执行write方法时打印的值就是0,与程序本来的语义相悖。JVM在指令重排时保证了volatile共享变量的局部有序,volatile修饰的共享变量的全部操做指令不管读写,volatile操做指令前的全部指令在排序后没法放在该指令后执行,volatile操做指令后的全部指令重排序后也不能放到该指令前执行。JVM内部经过在编译器生成字节码指令序列时插入内存屏障禁止特定规则的指令重排序。JVM为了保证volatile的局部有序遵循如下几条规则:ide

1.volatile变量的写操做要优先于对volatile变量的读操做;性能

2.转递性原则,若是A操做先于B操做,B操做先于C操做,那么A操做确定先于C操做。spa

相关文章
相关标签/搜索