最近在看Java内存模型时,看到了volatile关键字,现将该关键字的用法总结一下。volatile变量主要有两方面的特性:一是保证了此volatile变量对所有线程的可见性,二是volatile变量禁止指令重排序优化。
在解释volatile变量的第一个特性前,先来解释可见性(Visibility)这个概念。可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个改变。我们知道处理器与主内存之间一般都有高速缓存(Cache),这是因为处理的运算速度和内存的读取速度相差几个数量级。同样,在Java内存模型中,有相应的工作内存和主内存,可以把它们与高速缓存和主内存相对应。Java线程需要共享的变量存储在主内存中,每个Java线程都有自己的工作内存。线程、主内存、工作内存三者的关系如下图所示:
线程、工作内存、主内存三者的交互关系
在使用volatile变量前,都要从主内存内读取最新的值,用于保证能看见其他线程对volatile变量所做修改后的值。在每次修改volatile变量后都必须立刻同步回主内存中,用于保证其它线程可以看到自己对变量所做的修改。这是volatile变量的特殊规则,也是volatile变量可见性的由来。
无论是普通变量还是volatile变量都是通过主内存作为传递媒介的方式来实现可见性。普通变量与volatile变量的区别在于volatile变量的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此我们可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。但是应该注意的是 volatile变量的可见性并不能保证其运算在并发下是安全的。这是因为如果Java里面的运算并非原子操作,则volatile变量的运算在并发情况下是不安全的。例如:
public class VolatileTest{
public static volatile int race = 0;
public static void increase(){
race++;
}
private static final int THREADS_COUNT = 20;
public static void main(String[] args){
Thread[] threads = new Thread[THREADS_COUNT];
for(int i = 0; i < THREADS_COUNT; i++){
threads[i] = new Thread(new Runnable(){
@Override
public void run(){
for(int i = 0; i < 10000; i++){
increase();
}
}
});
threads[i].start();
}
//等待所有累加线程都结束
while(Thread.activeCount() > 1)
Thread.yield();//线程主动让出执行时间
System.out.println(race);
}
}
volatile变量只能保证可见性,并不能保证并发的安全。除了下面两种情况不需要加锁来保证原子性,其余的volatile变量运算仍然需要通过加锁来保证并发下的安全。
- 运算结果并不依赖变量的当前值,或着能够确保只有一个线程来修改变量的值;
- 变量不需要与其他的状态变量共同参与不变约束;
//此变量必须定义为volatile
volatile boolean initialied = false;
//以下代码为线程A执行
do_something();//线程A执行初始化相关的操作
....
initialied = true;//执行完初始化
//以下代码为线程B执行
//等待initialied为true,代表线程A初始化完了
while(!initialied){
sleep();
}
do_someting();
在上述代码中,volatile关键字确保了initialized变量不会被编译器优化,这样线程A初始化完之后才会触发线程B执行,而不会出现线程A没有初始化完线程B就开始执行的情况。
总之,volatile变量在多线程之间具有可见性,同时volatile变量不会被编译器优化;在某些情况下,volatile变量是线程之间同步的很好选择。