Java 开发, volatile 你必须了解一下

并发的三个特性

首先说咱们若是要使用 volatile 了,那确定是在多线程并发的环境下。咱们常说的并发场景下有三个重要特性:原子性、可见性、有序性。只有在知足了这三个特性,才能保证并发程序正确执行,不然就会出现各类各样的问题。java

原子性,上篇文章说到的 CAS 和 Atomic* 类,能够保证简单操做的原子性,对于一些负责的操做,可使用synchronized 或各类锁来实现。编程

可见性,指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其余线程可以当即看获得修改的值。缓存

有序性,程序执行的顺序按照代码的前后顺序执行,禁止进行指令重排序。看似理所固然的事情,其实并非这样,指令重排序是JVM为了优化指令,提升程序运行效率,在不影响单线程程序执行结果的前提下,尽量地提升并行度。可是在多线程环境下,有些代码的顺序改变,有可能引起逻辑上的不正确。多线程

而 volatile 作实现了两个特性,可见性和有序性。因此说在多线程环境中,须要保证这两个特性的功能,可使用 volatile 关键字。并发

volatile 是如何保证可见性的

说到可见性,就要了解一下计算机的处理器和主存了。由于多线程,无论有多少个线程,最后仍是要在计算机处理器中进行的,如今的计算机基本都是多核的,甚至有的机器是多处理器的。咱们看一下多处理器的结构图:app

这是两个处理器,四核的 CPU。一个处理器对应一个物理插槽,多处理器间经过QPI总线相连。一个处理器包含多个核,一个处理器间的多核共享L3 Cache。一个核包含寄存器、L1 Cache、L2 Cache。优化

在程序执行的过程当中,必定要涉及到数据的读和写。而咱们都知道,虽然内存的访问速度已经很快了,可是比起CPU执行指令的速度来,仍是差的很远的,所以,在内核中,增长了L一、L二、L3 三级缓存,这样一来,当程序运行的时候,先将所须要的数据从主存复制一份到所在核的缓存中,运算完成后,再写入主存中。下图是 CPU 访问数据的示意图,由寄存器到高速缓存再到主存甚至硬盘的速度是愈来愈慢的。
spa

了解了 CPU 结构以后,咱们来看一下程序执行的具体过程,拿一个简单的自增操做举例。线程

i=i+1;

执行这条语句的时候,在某个核上运行的某线程将 i 的值拷贝一个副本到此核所在的缓存中,当运算执行完成后,再回写到主存中去。若是是多线程环境下,每个线程都会在所运行的核上的高速缓存区有一个对应的工做内存,也就是每个线程都有本身的私有工做缓存区,用来存放运算须要的副本数据。那么,咱们再来看这个 i+1 的问题,假设 i 的初始值为0,有两个线程同时执行这条语句,每一个线程执行都须要三个步骤:code

一、从主存读取 i 值到线程工做内存,也就是对应的内核高速缓存区;

二、计算 i+1 的值;

三、将结果值写回主存中;

建设两个线程各执行 10,000 次后,咱们预期的值应该是 20,000 才对,惋惜很遗憾,i 的值老是小于 20,000 的 。致使这个问题的其中一个缘由就是缓存一致性问题,对于这个例子来讲,一旦某个线程的缓存副本作了修改,其余线程的缓存副本应该当即失效才对。

而使用了 volatile 关键字后,会有以下效果:

一、每次对变量的修改,都会引发处理器缓存(工做内存)写回到主存;

二、一个工做内存回写到主存会致使其余线程的处理器缓存(工做内存)无效。

由于 volatile 保证内存可见性,实际上是用到了 CPU 保证缓存一致性的 MESI 协议。MESI 协议内容较多,这里就不作说明,请各位同窗本身去查询一下吧。总之用了 volatile 关键字,当某线程对 volatile 变量的修改会当即回写到主存中,而且致使其余线程的缓存行失效,强制其余线程再使用变量时,须要从主存中读取。

那么咱们把上面的 i 变量用 volatile 修饰后,再次执行,每一个线程执行 10,000 次。很遗憾,仍是小于 20,000 的。这是为何呢?

volatile 利用 CPU 的 MESI 协议确实保证了可见性。可是,注意了,volatile 并无保证操做的原子性,由于这个自增操做是分三步的,假设线程 1 从主存中读取了 i 值,假设是 10 ,而且此时发生了阻塞,可是尚未对i进行修改,此时线程 2 也从主存中读取了 i 值,这时这两个线程读取的 i 值是同样的,都是 10 ,而后线程 2 对 i 进行了加 1 操做,并当即写回主存中。此时,根据 MESI 协议,线程 1 的工做内存对应的缓存行会被置为无效状态,没错。可是,请注意,线程 1 早已经将 i 值从主存中拷贝过了,如今只要执行加 1 操做和写回主存的操做了。而这两个线程都是在 10 的基础上加 1 ,而后又写回主存中,因此最后主存的值只是 11 ,而不是预期的 12 。

因此说,使用 volatile 能够保证内存可见性,但没法保证原子性,若是还须要原子性,能够参考,以前的这篇文章。

volatile 是如何保证有序性的

Java 内存模型具有一些先天的“有序性”,即不须要经过任何手段就可以获得保证的有序性,这个一般也称为 happens-before 原则。若是两个操做的执行次序没法从 happens-before 原则推导出来,那么它们就不能保证它们的有序性,虚拟机能够随意地对它们进行重排序。

以下是 happens-before 的8条原则,摘自 《深刻理解Java虚拟机》。

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操做先行发生于书写在后面的操做;
  • 锁定规则:一个 unLock 操做先行发生于后面对同一个锁的 lock 操做;
  • volatile 变量规则:对一个变量的写操做先行发生于后面对这个变量的读操做;
  • 传递规则:若是操做A先行发生于操做B,而操做B又先行发生于操做C,则能够得出操做A先行发生于操做C;
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个一个动做;
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  • 线程终结规则:线程中全部的操做都先行发生于线程的终止检测,咱们能够经过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  • 对象终结规则:一个对象的初始化完成先行发生于他的 finalize() 方法的开始;

这里主要说一下 volatile 关键字的规则,举一个著名的单例模式中的双重检查的例子:

class Singleton{  
    private volatile static Singleton instance = null;  
       
    private Singleton() {  
           
    }  
       
    public static Singleton getInstance() {  
        if(instance==null) {                //  step 1
            synchronized (Singleton.class) {  
                if(instance==null)          //  step 2
                    instance = new Singleton();  //step 3
            }  
        }  
        return instance;  
    }  
}

若是 instance 不用 volatile 修饰,可能产生什么结果呢,假设有两个线程在调用 getInstance() 方法,线程 1 执行步骤 step1 ,发现 instance 为 null ,而后同步锁住 Singleton 类,接着再次判断 instance 是否为 null ,发现仍然是 null,而后执行 step 3 ,开始实例化 Singleton 。而在实例化的过程当中,线程 2 走到 step 1,有可能发现 instance 不为空,可是此时 instance 有可能尚未彻底初始化。

什么意思呢,对象在初始化的时候分三个步骤,用下面的伪代码表示:

memory = allocate();  //1. 分配对象的内存空间 
ctorInstance(memory); //2. 初始化对象
instance = memory;    //3. 设置 instance 指向对象的内存空间

由于步骤 2 和步骤 3 须要依赖步骤 1,而步骤 2 和 步骤 3 并无依赖关系,因此这两条语句有可能会发生指令重排,也就是或有可能步骤 3 在步骤 2 的以前执行。在这种状况下,步骤 3 执行了,可是步骤 2 尚未执行,也就是说 instance 实例尚未初始化完毕,正好,在此刻,线程 2 判断 instance 不为 null,因此就直接返回了 instance 实例,可是,这个时候 instance 实际上是一个不彻底的对象,因此,在使用的时候就会出现问题。

而使用 volatile 关键字,也就是使用了 “对一个 volatile修饰的变量的写,happens-before于任意后续对该变量的读” 这一原则,对应到上面的初始化过程,步骤2 和 3 都是对 instance 的写,因此必定发生于后面对 instance 的读,也就是不会出现返回不彻底初始化的 instance 这种可能。

JVM 底层是经过一个叫作“内存屏障”的东西来完成。内存屏障,也叫作内存栅栏,是一组处理器指令,用于实现对内存操做的顺序限制。

最后

经过 volatile 关键字,咱们了解了一下并发编程中的可见性和有序性,固然只是简单的了解。更深刻的了解,还得靠各位同窗本身去钻研。若是感受仍是有点做用的话,欢迎点个推荐。

相关文章
相关标签/搜索