volatile有两个特性
- 确保volatile修饰的变量对所有线程可见。
- 禁止指令重排
确保volatile修饰的变量对所有线程可见
为了解释第一个特性,首先我们要知道java内存模型,java内存逻辑上分为工作内存和主内存(这里内存的定义和我们平常所说的java内存分为堆、栈、方法区等并不冲突)
java内存模型规定所有的变量都存在主内存中,每条线程还有自己的工作内存,工作内存中保存了该线程使用到变量的主内存的副本拷贝,线程对所有变量的修改都应该在工作内存中,而不能直接修改主内存中的变量。不同线程之间不互相操作对方工作内存中的变量,线程间变量之间的消息传递必须通过主内存。
图1:java线程、工作内存、主内存之间的关系
现有一业务场景如下:
主内存中有一个共享变量i,线程A的主要任务是修改共享变量i的值,线程B的主要任务是读取共享变量i的值。如果不对程序做同步处理,某时刻线程A将共享变量i的值修改为2。上文我们说到线程修改的是工作内存中主内存的副本的值,线程A修改共享变量i的值后还未将最新值同步回主内存中。而此时线程B读取了共享变量i的值。因此在并发的时候就可能出现问题。
如果将共享变量用volatitle修饰后,就可以避免这样的问题。现在我们来看看,volatile是怎么保证对所有线程的可见性的。
依然还是上面的业务场景。在线程A修改了共享变量i的值后,由于此变量被volatile修饰,所有虚拟机会将线程A中共享变量i的值立即同步回主内存中。线程B在读取共享变量i的值时,不会选择线程B工作内存中的副本值,而会重新读取主内存中共享变量i的值。所以volatile保证了对所有线程的可见性。理解volatile可见性的一个好方法是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步操作。
禁止指令重排。
指令重排是指编译器和处理器为了提高程序性能而对指令序列进行重新排序的一种手段。
public class VolatileTest {
int a = 0;
int i = 0;
boolean flag = false;
public void write() {
a = 1;
flag = true;
}
public void read() {
if(flag)
i = a + 1;
}
}
flag是一个标记,用来标记变量a是否已经被修改。假设有线程A和线程B,A执行write操作,B执行read操作,那么B在执行i = a + 1操作时是否能看到线程A在write操作中对a的修改呢?
不一定
处理器和编译器可能对程序指令进行重排序,程序执行顺序可能为
在这里多线程程序的语义被重排序破坏了,故在写多线程程序时不能认为写在前面的代码就一定会比写在后面的代码先执行。那程序执行的顺序应该按照什么规则进行判断呢?从JDK5开始,java使用新的JSR-133内存模型。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
- 程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。
- 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”指时间上的先后顺序。
- Volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
- 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
- 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始。
- 传递性:如果A happens-before B,且Bhappens-before C那么A happens-before C。
当变量被volatile修饰后,虚拟机通过在volatile加入内存壁障的手法从而达到禁止指令重排序的目的。