Java 语言中的 volatile 变量可以被看作是一种“程度较轻的 synchronized“;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。为了真正搞明白volatile的语义和用法,我们首先介绍下Java内存模型;内存间的交互操作;原子性、可见性、有序性;先行发生原则等概念。
1.Java内存模型
在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。不同的线程之间不能直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
线程、主内存、工作内存三者的交互关系如下图所示:
2.内存间交互操作
关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了8种操作来完成,虚拟机实现时必须保证以下每一种操作都是原子的、不可再分的(double、long类型的变量有些例外)。
① lock(锁定):作用于主内存变量,它把一个变量标识为一条线程独占的状态。
② unlock(解锁):作用于主内存变量,它把一个处于锁定状态的变量释放出来,释放后的变量才能被其它线程锁定。
③ read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程中的内存中,以便随后的load动作使用。
④ load(载入):作用于工作内存变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
⑤ use(使用):作用于工作内存变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作。
⑥ assign(赋值):作用于工作内存变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
⑦ store(存储):作用于工作内存变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
⑧ write(写入):作用于工作内存变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
3.原子性、可见性、有序性
●原子性(Atomicity):即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
在Java中,对基本数据类型的变量的读取和赋值操作都是原子性操作(long和double有非原子性协定,了解即可)。Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
●可见性(Visibility):可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
对于可见性,Java提供了volatile关键字来保证可见性。 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
●有序性(Ordering):即程序执行的顺序按照代码的先后顺序执行。
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。 在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
4.先行发生原则
Java内存模型具备一些"天然的"先行发生关系,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就没有顺序性保障,虚拟机可以随意地对它们进行重排序。
先行发生原则共有以下8个:
●程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作 (控制流顺序不是程序代码顺序)。●管理锁定规则:一个unLock操作先行发生于后面(时间上)对同一个锁的lock操作 。
●volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作 。
●线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作 。
●线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生 。
●线程终止规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行 。
●对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。
●传递性:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C 。
5.volatile关键字剖析
volatile 变量可以被看作是一种“程度较轻的 synchronized“,是Java虚拟机提供的最轻量级的同步机制。
5.1 volatile关键字的两层语义
当一个变量定义为volatile之后,它就具备了两层语义:
1)保证此变量对所有线程的可见性,即一个线程修改了某个变量的值,新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
综述:对于被volatile修饰的变量,volatile不能保证原子性、可以保证可见性、能在一定程度上保证有序性
5.2 volatile的使用场景
通常来说,使用volatile必须具备以下2个条件:
1)运算结果不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
2)变量不需要与其他的状态变量共同参与不变约束。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
参考:《深入理解Java虚拟机》