Java内存模型是围绕着原子性、有序性和可见性来展开的。为了在适当的场合,确保线程间的有序性、可见性和原子性。Java使用了一些特殊的操作或关键字来声明、告诉虚拟机,在这个地方要尤其注意,不能随意变动优化目标指令。关键字volatile就是其中之一。
volatile翻译成中文是“易变的、不稳定的”的意思,这就是它的语义。
当使用关键字volatile声明一个变量时,就等于告诉虚拟机这个变量极有可能会被某些程序或线程修改。为了确保这个变量被修改后,应用程序范围内的所有线程都能够看到这个改变,虚拟机就必须采用一些特殊手段来保证这个变量的可见性等特点。
比如,根据编译器的优化规则,如果不使用关键字volatile声明变量,那这个变量在被修改后,其他线程可能并不会被通知到,甚至在别的线程中,看到变量的修改顺序都会是反的。一旦使用关键字volatile,虚拟机就会特别小心的处理这种情况。
在前面介绍原子性时,再结合这里可以看出,volatile对保证操作的原子性有非常大的帮助。但需要注意的是volatile并不能代替锁,它也无法保证一些复合操作的原子性。比如下面的例子,通过volatile是无法保证i++的原子性操作的。
static volatile int i = 0;
public static class PlusTask implements Runnable{
@Override
public void run(){
for (int k = 0; k < 10000; k ++)
i ++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i ++){
threads[i] = new Thread(new PlusTask());
threads[i].start();
}
for(int i = 0; i < 10; i ++){
threads[i].join();
}
System.out.println(i);
}
上述代码中,打印的结果是变化的。其实在run()方法的中i++原子性的,其最终结果是100000(10个线程累加10000)。但实际上,打印的结果总是变化的,且小于100000。
此外,volatile也能保证数据的可见性和有序性。看下面的例子:
public class NoVisibility {
private static boolean ready;
private static int number;
public static class ReaderThread extends Thread{
@Override
public void run(){
while (!ready);
System.out.println(number);
}
}
public static void main(String[] args) throws InterruptedException{
new ReaderThread().start();
Thread.sleep(1000);
number = 42;
ready = true;
Thread.sleep(10000);
}
}
上述代码中,ReaderThread线程只有在ready=true时,才会打印number的值。它通过ready变量判断是否应该打印。在主线程中,开启ReaderThread后,就为number和ready赋值,并期望ReadyThread能够看到这些变化并将数据输出。
在虚拟机的Client模式下,由于JIT(即时编译)并没有做足够的优化,在主线程修改ready修改变量的状态后,ReaderThread可以发现这个改动,并退出程序。但在Server模式下,由于系统优化的结果,ReaderThread线程无法“看到”主线程中的修改,这显然不是我们想看到的结果。这个问题就是一个典型的可见性问题。
注意:可以使用Java虚拟机参数-server切换到Server模式。
和原子性问题一样,只要简单的使用vloatile声明ready变量,告诉Java虚拟机,这个变量可能会在不同的线程中修改 。这样就可以顺利解决这个问题了。
注:以上内容参考《实战Java高并发程序设计(第2版)》。