Java并发编程之volatile关键字
简介
volatile关键字主要是用来解决共享变量内存可见性问题和CPU指令乱序执行问题。
下面通过一个实例来说明下这两个问题导致的原因和volatile如何解决这两个问题。
volatile的使用
public class TaskRunner {
private static int number;
private static boolean ready;
private static class Reader extends Thread {
@Override
public void run() {
while (!ready) {
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args) {
new Reader().start();
number = 42;
ready = true;
}
}
上面程序的执行结果除了延迟一段时间正常输出42之外,还可能存在以下两种结果:
1.线程永远不会停止
这种情况属于前面说的共享变量内存可见性问题。可见性是指当一个线程修改了某一个共享变量的值时,其他线程是否能够立即知道这个修改。
如下Java内存模型的抽象图所示,线程之间的共享变量都存储在主内存中,但每个线程本地内存都会存有共享变量的副本。假设前面例子中主线程和子线程分别在不同的CPU里,主线程将ready的值设置为true,这个值并不会立刻同步到主内存,所以子线程也就不能读取到最新的值,而读取的是本地内存变量副本的值。
2.线程结束,输出结果number值为0
这种情况属于前面说的CPU指令乱序执行问题。CPU的速度至少比内存快100倍,为了提升效率,会打乱原来的执行顺序,会在一条指令执行过程中,去同时执行另一条指令,当然乱序执行的前提是两条指令没有依赖关系。
在单线程的情况下,重排序能够保证乱序执行结果和顺序执行结果是一致的,但是在多线程的情况下就可能会有问题了。如上可能会出现ready = true先执行,而number = 42则刚好在输出语句后执行。
使用volatile避免出现上述问题
public class TaskRunner {
private volatile static int number;
private volatile static boolean ready;
//...
}
1.当一个变量被声明volatile时,线程对它的修改会直接刷新到主内存,其他线程对它的读取也是直接从主内存读取,这样就能保证所有线程读取到这个变量的值都是一致的。
2.volatile能够禁止指令重排序,在被声明volatile变量上面执行的指令不可以乱序。
volatile和synchronized的比较
volatile是轻量级的synchronized,它能保证可见性和有序性,但它不能保证原子性。之所以说volatile是轻量级的synchronized,是因为volatile的执行效率要更高,而synchronized是独占锁,会有锁的开销。
synchronized能够保证可见性,当线程进入synchronized块时,会先清空本地内存中的变量值,然后从主内存中读取最新的值。当线程退出synchronized块时,会将本地内存中的变量值同步到主内存。
synchronized块可以看作一个整体,同一时间只有一个线程运行synchronized块代码,不会被其他线程干扰,所以也就能保证原子性和有序性。