Volatile中文译为【易变的、不稳定的】,它被用来修饰那些被不同的线程访问和修改的变量。
学习volatile我们首先要来看一下Java内存模型(即Java Memory Model,简称JMM)。
Java内存模型中规定了所有的变量都存储在主内存中,每条java线程各自拥有自己的工作内存,线程的工作内存中保存了该线程使用到的主内存中的共享变量的副本,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成,线程、主内存和工作内存的交互关系如上图所示。
Volatile 修饰的变量即为主内存中的共享变量,当一个共享变量被volatile
修饰后,那么当这个变量在工作内存发生了变化后,必须要马上写到主内存中,而线程读取到是volatile修饰的变量时,必须去主内存中去获取最新的值,而不是读工作内存中主内存的副本,这就有效的保证了线程之间变量的可见性。
这就是volatile的第一个特性,内存可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
我们来看一段代码:
1 | public class VolatileTest01 { |
我们会发现,当线程B执行语句
stop=true;后,会中断线程A的执行。但有没有可能A出现死循环呢?答案是有的,即当线程B更改了stop变量之后还没来得及将stop变量写入主存当中,线程B就转去做其他事了,这时线程A由于不知道线程B对stop变量的更改,因此会一直循环下去,虽然这种情况发生即概论很低,但是一旦发生了将造成很严重的后果。
使用volatile关键字后我们便可以解决这个问题,但是这样就可以保证线程安全了吗?答案是不能,我们来看一下下面这段代码:
1 | public class VolatileTest05 { |
通过上面的代码我们创建10个线程,每个线程中让num自增100次。执行后你会发现结果并不是我们想的1000,而是小于1000.这是为什么呢?因为volatile并不能让一个操作变成原子操作。Num++本身并不是原子操作,转换为字节码是这样的:
getstatic //读取静态变量(num)
iconst_1 //定义常量1
iadd //num增加1
putstatic //把num结果同步到主内存
虽然每次get后得到的都是最新变量值,但是进行idd的时候,由于非原子性操作,其他线程可能已经将num++执行了很多次,此时这个线程更新出的num便已经不是我们所期望得到的num值。
JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,为了在不改变程序执行结果的前提下,优化程序的运行效率会对现有的指令顺序进行重新排序。
Volatile第二个特性即是可以防止指令重排序。
我们来看下面这个例子:
boolean contextReady = false;
在线程A中执行:
context = loadContext();//初始化context
contextReady = true;
在线程B中执行:
while( ! contextReady ){
sleep(200);
}
doAfterContextReady (context);
但如果一旦线程A中的指令重排序,会变成这样的执行顺序:
boolean contextReady = false;
线程A:
contextReady = true;
context = loadContext();//初始化context
线程B:
while( ! contextReady ){
sleep(200);
}
doAfterContextReady (context);
我们发现context并没有进行初始化线程B跳出循环等待后便执行了doAfterContextReady
(context)方法,程序报错。
此时我们可以给contextReady 增加volatile方法,以此解决关于contextReady
的指令重排序的问题。