http://book.51cto.com/art/201306/397838.htm
了解可见性:理解内存栅栏
《Java虚拟机并发编程》第1章并发的威力与风险,在本章中,我们将快速回顾一下并发编程技术的来龙去脉,并讨论在实践过程中可能遇到的一些风险。在本章末尾,我们将简单展望一下本书后面将会介绍的那些令人兴奋的并发编程技术。本节为大家介绍可见性:理解内存栅栏。
1.3.3 了解可见性:理解内存栅栏
上面那个示例存在的问题是,主线程对其字段done所做的变更对新创建出来的线程不可见。造成这种现象的首要原因是,JIT编译器可能对新线程代码里的while循环进行了优化,并因此导致新线程在线程上下文中无法看到变量done的变化。此外,新线程可能会只从其寄存器或本地cache中读取标记变量done的值,而不是每次都跑去速度更慢的内存里进行操作。基于上述原因,新线程就无法看到主线程对其标记变量值的变更了,详情请参阅下文“什么是内存栅栏”。
如果想要快速修复此问题,只需要将变量done标记为volatile就可以了。具体做法是将
改为:
关键字volatile的作用是告知JIT编译器不要对被标记变量执行任何可能影响其访问顺序的优化。该关键字警告JIT编译器,该变量可能会被某个线程更改,所以任何对该变量的读写访问都需要忽略本地cache并直接对内存进行操作。之前我将这个改动称为快速修复,是因为如果我们将所有变量都标记为volatile的话,虽然可以完全规避此类问题,但却会使每次变量访问都要跨越内存栅栏并最终导致程序性能下降。此外,在多个字段被多个线程并发访问的场景下,由于针对每个volatile字段的访问都是各自独立处理的,并且也无法将这些访问统一协调成一次访问,所以volatile关键字无法保证整体操作的原子性。该问题所造成的后果是,线程很可能对某些字段只能看到其中间结果,而对另一些变量则看到的是最终的变更结果。
为了解决这个问题,我们可以屏蔽对变量的直接访问,并将所有访问都引导为通过同步的getter和setter函数来进行,具体代码如下所示:
关键字synchronized在这里起到了至关重要的作用。由于synchronized是为数不多的几个可以令线程在进入和离开同步区块时都跨越内存栅栏的原语之一,所以如果多个线程在相同的实例对象上进行同步并且先申请到对象锁的线程完成了对实例对象的操作,则后面申请到对象锁的线程将肯定可以看到前面完成操作的线程所做的变更。再次提醒,要了解该问题的详情请参阅下文“什么是内存栅栏”。
Joe问:什么是内存栅栏?
简单来说,内存栅栏(Memory Barrier)就是从本地或工作内存到主存之间的拷贝动作。
仅当写操作线程先跨越内存栅栏(参见附录2中Doug Lea所著《The JSR-133 Cookbook for Compiler Writers》)而读线程后跨越内存栅栏的情况下,写操作线程所做的变更才对其他线程可见。关键字synchronized和volatile都强制规定了所有的变更必须全局可见,该特性有助于跨越内存边界动作的发生,无论是有意为之还是无心插柳。
在程序运行过程中,所有的变更会先在寄存器或本地cache中完成,然后才会被拷贝到主存以跨越内存栅栏。此种跨越序列或顺序称为happens-before,详情请参阅附录2中的“Java内存模型”以及Brian Goetz的著作《Java Concurrency in Practice》[Goe06]。
写操作必须要happens-before读操作,即写线程需要在所有读线程跨越内存栅栏之前完成自己的跨越动作,其所做的变更才能对其他线程可见。
Java并发API中很多操作都隐含有跨越内存栅栏的含义:volatile、synchronized、Thread中的函数如start()和interrupt()、ExecutorService中的函数以及像CountDownLatch这样的同步工具类等。