上一篇我们说到了用volatile关键字来保持多线程中共享变量的可见性,今天我们再来深入研究一下可见性是怎么实现的。
可见性实现:上一篇文章我们提到了要实现可见性就必须在使用共享变量时到主存去取出新值,更改完共享变量以后把新值及时写回主存。其实这就对应了两个操作,刷新处理器缓存与冲刷处理器缓存,刷新操作就是取新值(相对新值),冲刷就是把值写回主存。 大家有没有注意到我括号里面的东西,相对新值,为什么说是相对新值呢?
相对新值是对应于新值而言的,简单点说,我们从主存中取出的值是相对新值,在不加锁的情况下,一个线程从主存中取出值之后,另一个线程此刻可能正在修改该变量的值,所以,其实我们读到只是此时此刻主存中最新的,但可能只是因为其它线程并还没来得及写回主存里面,所以此时的操作就可能带来错误。那要怎么修改呢?其实这就是第一篇文章讲到的:互斥访问,我在操作共享变量的同时,其他线程就不能操作,可以通过加锁来实现。那么我们在读主存里的值得时候就一定是新值,操作就不会有任何问题。
为了检验大家对这块只是掌握情况,我贴出一段代码大家来参考一下:摘自《Effective Java》
前提是在多线程情况下:
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber(){
return nextSerialNumber++;
}
这个方法要做的事很简单,保证多线程访问的时候,每次都能返回一个递增的序号(不超过2^23个调用),我们用volatile来使每个线程调用一次加1之后对其它线程都是可见的,以此来实现累加。OK,怎么样,你觉得会有错吗?是不是看起来还可以?
答案是肯定会出现不一致的情况,如果不明白为什么,那还是没把原子性操作搞清楚。
我们先来明白两点:
1.上述代码并没有加锁(互斥访问)。也就是说我的线程可以毫无阻碍的修改nextSerialNumber的值。
2.nextSerialNumber++不是原子操作。就像我们的i++一样,它可以分解为i=i+1;我们说过对于不是原子性的操作我们要通过加锁等机制来使其具有原子性。
所以,上述代码就很可能出现同时修改。而volatile值保证读取的是相对新值,而不是新值,这也就是很多书上说的,volatile不保证原子性。volatile完全可以单独拿出一章来讲,敬请期待。
那么修改方法就很多了,可以加锁,但对于一个简单的i++操作这是没必要的,开销太大。我们可以使用原子变量类Atomic。
如AtomicInteger。这个类已经帮我们底层实现好了,可以放心用。
好的,本章讲了可见性的实现策略,刷新和冲刷,但其实还是隔靴搔痒,比较表面,下一章我将会讲到第三个重要特性:
有序性,在这一章里面我会更加细节的来讲这一块。咱们下一章再见!!
网友评论