Java虚拟机内存模型
了解Java虚拟机的内存模型,有助于我们明白为什么会发生线程安全问题.
上面这幅图是<深入理解Java虚拟机-JVM高级特性与最佳实践>的书中截图.
线程共享的变量会保存在主内存中(Main Memory).
而线程共享的变量的副本会保存在每个线程各自的工作内存中(Working Memory).
线程对于共享变量的所有操作(读取,赋值等)都必须在工作内存中进行,不能直接读写主内存的变量.
不同的线程之间,也无法访问其他线程的工作内存.线程之间的变量传递需要通过主内存来完成.
所以发生线程安全问题的根源就在于,当共享变量在主内存中被修改后,有的线程的工作内存保存的还是该被修改的共享变量的旧的值,就导致了数据不一致的后果.
Volatile型变量
被Volatile修饰的变量,是轻量化的解决部分线程安全问题的方法,它具有2种特性:
1. 保证此变量对于所有线程的可见性,指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的.
但是这里所说的立即得知,也并不能保证Volatile就能完全避免线程安全数据一致的问题.
private volatile int race = 0; private void increase() { race++; }
比如上面代码,如果多线程的调用increase()方法,是并不能保证线程安全,而且是很有可能发生数据不一致的问题.
原因在于,虽然volatile保证了race变量的修改被其他线程立即得知,但是本身race++这行代码并不是原子操作.实际代码编译后会变成多条字节码.
volatile的使用场景需要符合2条规则:
1) 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值;
2) 变量不需要与其他的状态变量共同参与不变约束.
volatile型变量的经典使用场景是如下这种.当doSomthing()方法被多线程调用时候,只需把shutDown=true之后,所有线程就不会在进行下一轮的while.
private volatile boolean shutDown = false; private void doSomething() { while (!shutDown) { // do something } }
2. 使用Volatile变量能禁止指令重排序.
普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致.因为在一个县城的方法执行过程中无法感知到这点.
public VolatileExample { Map configOptions; char[] configText; volatile boolean initiallized = false; private void init() { configOptions = new HashMap(); configText = readonfigFile(fileName); processConfigOptions(configText, configOptions); initialized = true; } private void do() { while (!initialized) { sleep(); } doSomethingWithconfig(); } }
上面这段代码,假设init()方法和do()方法运行再不同的线程中.如果initialized变量没有使用volatile修饰的话,init()方法可能由于指令重排序的优化(指令重排序是机器级的优化操作,提前执行时指这句话对应的汇编代码会被提前执行),导致最后一句代码initialized=true被提前执行.这种情况的后果就是do()方法因为initialized=true就跳过了while循环去执行doSomethingWithConfig()了,但其实可能init()的方法还未运行结束.
而volatile关键字可以避免此类情况的发生.