多线程中共享对象的可见性
在阅读《java并发编程实战》的第三章的时候,看到书中的一个例子,随在Eclipse中执行看看效果。示例代码如下:
public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while(!ready) { Thread.yield(); } System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; } }
书中的解释是,这个程序的执行结果除了打印出42以外,还有两外另种情况:一时无限循环下去,二是打印出0来。其中打印42很好理解,因为,在启动ReaderThread线程以后,如果还没有设置ready为true,那么ReaderThread会一直循环,直到读取ready的值为true,然后打印出结果42来。
无限循环这个结果,通过代码来测试是很难观察到的,但是通过书中的分析,这种情况是存在的。这个示例中的number和ready,会被两个线程访问,其一是运行该程序的main线程,其二是ReaderThread线程,所以这两个变量可以称之为共享变量,由于java虚拟机自己的缓存机制,在缺少同步的情况下,会将number和ready的数值缓存在寄存器中,这就会导致ReaderThread线程在某些情况下读取不到最新的值,这样即使在main方法中将ready设置为true了,但是ReaderThread线程读取的ready值仍然为false,这样就会导致一直循环下去。
上面的这个示例很难观察到这种情况,所以我对其进行了改造,可以看到另外一种与我们预期不相符的情况:
public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while(!ready) { System.out.println(System.currentTimeMillis()); Thread.yield(); } System.out.println(number); } } public static void main(String[] args) { for(int i=0;i < 100;i++) new ReaderThread().start(); number = 42; ready = true; } }
对示例代码做了两处改动,第一,在ReaderThread线程的while循环中打印当前时间,第二,在main方法中增加了一个循环,创建了100个ReaderThread线程并启动线程。然后我们看看运行结果:(这里只列出了部分结果)
... 1354960834108 42 1354960834108 1354960834108 42 1354960834108 42 1354960834108 1354960834108 1354960834108 1354960834108 42 42 ...
从打印的结果来看,就会发现问题,为什么在某些线程打印了42以后,有些线程仍然在打印时间?
如果某个线程打印出了42,说明main方法已经执行完毕,即变量ready的值已经设置为true了,那么这以后其它的线程打印的结果应该都是42了,但这里的结果是有些线程读取的ready值仍然为false,这就说明了java虚拟机会对线程中使用到的变量进行缓存,所以就出问题了。
java虚拟机缓存变量,是出于性能的考虑,并且在单线程程序中,或者不存在共享变量的多线程程序中,这都不会出现问题。但是,在有共享变量的多线程程序中,就会发生问题,这里就涉及到共享对象的可见性了,也就是在没有使用同步机制的情况下,一个线程对某个共享对象的修改,并不会立即被其它的线程读取到。上面的代码之所以会出问题,就是因为ReaderThread线程,没有读取到main线程对ready变量修改后的值。要解决上述问题,可以通过在main方法和ReaderThread线程中的run方法中,给访问number和ready值的代码块中加锁来解决。
另外一种结果打印出0来,这个暂时还不是很明白,书中的解释是java虚拟机的内存模型允许编译器对操作顺序进行重排序,并将数值缓存在寄存器中,这样可能在读取到ready修改后的值后,却仍然读取了number的旧值,从而打印出了int的默认值0来。
已有 0 人发表留言,猛击->> 这里<<-参与讨论
ITeye推荐