volatile 解析

内存模型

计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存。

也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。

为了解决缓存不一致性问题,通常来说有以下2种解决方法:

  • 通过在总线加LOCK#锁的方式
  • 通过缓存一致性协议

这2种方式都是硬件层面上提供的方式。

并发编程中的三个概念

原子性 可见性 有序性

  • 原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就不执行
  • 可见性:可见性是指多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看的到修改的值。
  • 有序性:即程序执行的顺序按照代码的先后顺序执行。

Java内存模型

在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。

主要来看有序性,

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

另外,Java内存模型具有一些先天的有序性,即不需要任何手段就能够保证的有序性,也就是happens-before规则,他是JMM对程序员的一种承诺。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

下面就来具体介绍下happens-before原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
1
程序次序规则就是一段程序代码的执行在单个线程中看起来是有序的,这个规则只能用来保证程序在单线程中执行结果的正确性,但是无法确保程序在多线程中执行的正确性。
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
1
无论在单线程还是多线程中,同一个锁如果出于被锁定的状态,那么必须先对锁进行了释放操作,才能继续进行lock操作。
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
1
如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
1
happens-before规则具有传递性

分析volatile 关键字

volatile关键字的两层语义

一旦一个共享变量被volatile 修饰之后,那么就具备了两层语义

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的
  • 禁止进行指令重排序。

禁止指令重排序有两层意思,

  • 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行。

  • 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

    一个简单的例子

    1
    2
    3
    4
    5
    6
    7
    //x、y为非volatile变量
    //flag为volatile变量
    x = 2; //语句1
    y = 0; //语句2
    flag = true; //语句3
    x = 4; //语句4
    y = -1; //语句5

    由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

      并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

    volatile 的实现原理

    “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

    lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

    • 1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
    • 2)它会强制将对缓存的修改操作立即写入主存。
    • 3)如果是写操作,它会导致其他CPU中对应的缓存行无效。