// 先上代码
1 public class NoVisibility { 2 private static boolean ready; 3 private static int number; 4
5 private static class ReaderThread extends Thread { 6 public void run() { 7 while(!ready) { 8 Thread.yield(); 9 } 10 System.out.println(number); 11 } 12 } 13 14 public static void main(String[] args) { 15 new ReaderThread().start(); 16 number = 100; 17 ready = true; 18 } 19 }
上边的代码,如果直接运行,main进程首先开始ReaderThread进程,再去设置ready为true。
直观感受应该是当main进程将ready设为true后,ReaderThread进程就会跳出while循环,从而输出number值100。
但实际上,程序可能一直无限循环,或是输出的值为0. 因为这里没有用到同步机制,main进程写的ready和number值,不能保证ReaderThread看到。
为什么?
这里在多线程的背景下,JVM可能会进行底层的字节代码的优化和重排序,所以developer如果没有设计合理的同步机制,就不能确保代码的执行顺序是按照你所写的那样。
怎么做?
最基本的方法,就是用Locking机制,分别在读取和设置变量的方法上设置同一个锁,这就能保证 变量在进程A释放锁前的改动, 进程B在获得锁(同一个锁)后也能读取到。
另外一种常见的方法,就是将 可能被不同的进程读取的变量用volatile修饰,volatile可以近似看作一种轻量级的同步机制。 此外volatile用来告诉编译器和运行库这个变量会在多个进程间共享,
不要将它缓存在寄存器或是其他进程看不到的cache里。
需要注意的是volatile只能保证可见性,不能保证原子性(比如 volatile int a = 0; a++; 两个进程可能都同时读到0),所以它是轻量级的同步机制。
volatile的实现原理
如果大家有兴趣查看代码JIT生成后的汇编指令,会发现针对volatile的变量的写操作,会有一个Lock指令,这是用来实现内存屏障的,保证如果一个处理器修改了变量值,会直接将值写回到内存,其他的处理器对应的缓存也会失效,需要重新从内存中读取,这样就保证所有的处理器读到的值,都是最近的变量值。