一、简述:
关键字Volatile是JAVA虚拟机提供的最轻量级的同步机制,但是它并不容易完全被正确、完整的理解,以致于许多程序员在遇到需要处理多线程数据竞争的时候一律使用synchronized来进行同步,了解volatile变量的语义对后面了解多线程操作的其他特性很有意义。
二、应用:
当一个变量被定义为volatile之后,它将具备两种特性:
1. 保证此变量对所有线程的可见性:
这里的可见性是指当一个线程修改了变量的值,新值对于其他线程来说是可以立即得知的,而普通变量不能做到这一点,普通变量的值在线程间传递需要通过主内存来完成。
volatile变量在各个线程中是一致的,但基于volatile变量的运算在并发下却不一定是安全的,参见如下案例:
public class Volatile { public static volatile int count = 0; public static void increaseCount() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }; count++; } public static void main(String[] args) { for (int i = 0; i < 1000; i++) { new Thread(new Runnable() { @Override public void run() { increaseCount(); } }).start(); } System.out.println("count值: " + count); } }
预想的返回值应该是1000,但实际的返回结果值却是小于1000的值,原因在于:
利用javap发编译后得到代码清单,对应的increaseCount()对应的字节码指令如下:
public static void increaseCount(); Code : Stack=2, Locals=0, Args_size=0 0: getstatic 3: iconst_1 4: iadd 5: putstatic 8: return
当getstatic指令把count的值取到操作栈顶是,volatile关键字保证了count在此时是正确的,但是在执行iconst_1、iadd这些指令时,其他线程可能已经把count的值加大了,而在操作栈顶的数据就变成了过期的数据,所以putstatic执行后就可能把较小的count值同步回主内存中了。
所以,由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,仍需要通过加锁来保证原子性:
1)运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量值;
2)变量不需要与其他的状态变量共同参与不变约束。
2. 禁止指令重排序优化
普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致,如下场景:
boolean flag = false;
线程A:
doSomeThing();
flag = true;
线程B:
while(!flag){
doSomeThingElse();
}
A.doSomeThing();//调用A线程的方法
如果定义flag变量时如果没有使用volatile修饰,就有可能由于指令重排序的优化,导致位于线程A中最后一句"flag = true"被提前执行,这样在线程B中调用的A的方法就有可能出现错误,使用volatile可以避免此现象的发生。
三、总结
volatile的同步性能机制要优于synchronized,但由于虚拟机对锁实行的许多消除和优化,实际上我们很难量化的认为volatile会比synchronized快多少,但volatile跟自己相比而言:volatile变量读操作的性能消耗与普通变量几乎没什么差别,但是写操作则可能慢一些,因为他需要在本地代码中插入许多内存屏障来保证处理器不发生乱序执行。