volatile关键字是一种比锁机制稍弱点的同步机制,volatile变量会被编译器以及JVM运行时注意到是一个共享变量,被它修饰的变量具有两大特征:第一,保证了不同线程对该变量的可见性。被它修饰的变量在线程中被修改后会立马写入主存中,在Java内存模型中,volatile变量的写入操作与读取操作同时到达的时候,将先进行写入操作,来保证数据的一致性。
第二点就是阻止了指令重排序,重排序是引起线程间有序性问题的罪魁祸首,简单的描述重排序问题是Java内存模型使得不同的线程看到的操作执行顺序是不一样的。在讨论内置锁的时候我们谈论了线程之间的可见性问题,但事实上更糟的是,线程之间不仅仅有着更改后的数据无法及时被使用者看到,还有指令的在不同线程之间重排的问题,这就导致了不仅在开始时是错的,运行时更是错上加错的结果。也就是说在线程中会发生不按程序顺序执行的指令,本来A在B之后执行,但是因为重排序会造成A在B之前执行。比如示例程序:
public class Test {
static String a="蕾姆";
static String b="拉姆";
static String x="托尔";
static String y="康娜";
public static void main (String args[]) throws InterruptedException {
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
a="艾米莉亚";
x=b;
}
});
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
b="艾米莉亚";
y=a;
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(x+"\n"+y);
}
}
这份程序有可能输出的是:拉姆、蕾姆。但是这个结果很难测出来,现在的CPU有些阻止了写写重排序,但是一定要知道有这回事,而且也很难预测这个程序的结果,充满着很多种可能。加了volatile修饰的变量则保证了这种可能不会发生,需要注意的是,重排序不会改变对该指令前面成员有依赖的指令顺序。比如:
int a=10; 1式
int b=6; 2式
int c=a+b; 3式
1式和2式可以相互替换,但是3式对前两者有依赖,所以顺序是死的。但是在上一个例子中,a、x等变量也依赖着前面的成员啊?因为每个线程有着自己的单独的内存空间进行存贮与运算,a、x在线程的空间内是主存中的副本,也就是说在这个空间,它们没有依赖项,所以可能发生重排序。这也就是为什么重排序影响不到单线程却能影响到多线程的原因。
当然,你也可以用锁来实现volatile这两大特性,但是比其的开销要大一些。volatile的一个很明显的缺点就是它没有实现原子性操作,也就是这一点,推荐大家尽可能的少使用volatile。有一点注意的是:
volatile i++;
这个代码虽然看起来像是原子性的操作,但是其中却包括三个操作:读取i的值,计算i+1的值,写入i的值,所以必须利用锁来保证其原子性。