volatile
volatile和synchronized关键字在并发编程中扮演着非常重要的角色,volatile可以说是轻量级的synchronized,它在多处理器开发中保证了共享变量的可见性(一个线程修改某个变量的值,对于另一个线程是可见的)。
volatile的特性
- volatile禁止指令的重排序
重排序:指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段
源代码从编译到执行会经历三类重排序:编译器重排序、指令集并行重排序、内存系统重排序,但是有些时候指令是不能重排序的,比如存在数据依赖性,JMM采用内存屏障来禁止特殊指令的重排序。下图是JMM针对编译器的重排序规则表: - volatile实现共享变量的可见性,如下图所示的java内存模型,对于共享编程,每个线程都会在本地内存中保留一个副本,线程执行时对副本进行操作,那么可想而知,如果线程A对共享变量的副本进行修改,线程B是不能得到修改后的值的,但是volatile的写-读内存语义保证可以强制将更改后的变量值刷新到共享内存,并使线程B的共享变量副本失效,实际上这也是线程A直接对线程B的通信。
我们要先明白happens-before规则:JDK5开始使用JSR-133内存模型,这个模型采用happens-before的概念操作内存的可见性,这个规则包含以下规则:- 一个线程的操作happens-before这个线程的后续操作
- 监视器锁规则:对一个锁的加锁happens-before与这个锁的解锁
- volatile变量规则:对volatile的写happens-before于后续对这个变量的读
- 传递性:Ahappens-before B,且B happens-before C,则A happens-before C
举个例子:如下代码,根据happens-before规则,1 hb 2,3 hb 4 ,根据volatile规则,2 hb 3,根据传递性 1 hb 4,但是JSR133之前的模型虽然不让voltile变量重排序,但是volatile与普通变量可以,就会导致下述代码发生变化,线程A先执行volatile,然后线程B执行volatile然后读去a(此时a还未被更改),就破坏了语义。
最后我们对volatile的语义进行验证:
- 我们先不对共享变量加volatile关键字,更改线程A的值,看B是否会得到正确的值
public boolean isBreak = false; //第二步加上volatile的修饰 //public volatile boolean isBreak = false public boolean getBreak() { return isBreak; } public void setBreak(boolean aBreak) { isBreak = aBreak; } public class A extends Thread{ @Override public void run() { System.out.println("loop start"); while (!isBreak){ } System.out.println("loop end"); } } public class B extends Thread{ @Override public void run() { setBreak(true); } } public static void main(String[] args) throws InterruptedException { VolatileTest volatileTest = new VolatileTest(); volatileTest.new A().start(); //进入循环 Thread.sleep(1000); volatileTest.new B().start(); //等待修改的同步 Thread.sleep(1000); System.out.println("isBreak:"+volatileTest.getBreak()); }
结果如下图A:和我们预料的一样,线程A没有得到最新值,说明普通变量不具备可见行,然后添加volatile修饰的结果如图B所示
- volatile保证单次读/写的原子性对单个volatile变量的读/写具有原子性: 可以理解为使用锁对单个变量的读写进行了锁操作,像a++这样的操作就不能保证原子性