Java 并发编程:volatile 能否保证数据的同步
volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在Java 5之后,volatile关键字才得以重获生机。
关于存储介质
作为Java开发我们都了解Java内存模型,JMM为了提高执行性能引入了工作内存和主存两个概念。在继续讨论之前必须先搞清四种存储介质:寄存器、高级缓存、RAM和ROM。RAM与ROM大家都比较熟悉了,可以看成是我们经常说的内存与硬盘。寄存器属于处理器里面的一部分,而高级缓存cache是CPU设计者为提高性能引入的一个缓存,也可以说是属于处理器的一部分。
为什么需要他们
在利用CPU进行运算时必定涉及操作数的读取,假如CPU直接读取ROM,那么这个读取速度简直是无法忍受的,于是引入了内存RAM。这样做确实让速度提高了很多,但由于CPU发展十分迅猛,另一方面RAM的发展受到技术及成本的限制而发展缓慢,此时就产生了一个很难调和的矛盾:CPU运算速度比从RAM读取数据的速度快了几个数量级。木桶原理我们都很熟悉的了,桶的容量大小取决于最短的那块。由于存在这个矛盾,它必将会影响处理器的效率,于是又引入了高级缓存。直接在CPU添加了几个不同级别的缓存,虽然它们的速度无法与寄存器相比,但是速度已经提升很多,基本能跟CPU的计算速度相匹配。
总结成一句话就是:为了解决CPU运算速度与读取速度的矛盾,引入了多种存储机制。读取速度快慢的排序如下:寄存器>cache>RAM>ROM。用一个比较好理解但不完全正确的概念来解释。因为寄存器是离CPU最近的,所以读取最快。高速缓存次之,RAM第三,ROM离得最远,自然速度最慢。当然不能完全用距离来说明这个问题,但用距离是比较好理解的。另外的影响因素还包括硬件设计不同、工作方式不同。
介质如何工作
机器的四种存储介质是有关系的,一般程序运行时会将ROM相关的程序数据都读进RAM中,而需要运算的数据或运算过程中即将要用到的数据则会被读进高速缓存或寄存器中。假如要进行的运算所需要的所有数据及指令都在寄存器和高速缓存中,则这个运算过程则表现得非常平坦。此时不存在性能瓶颈,因为运算速度跟读取速度基本匹配。
CPU读取数据的顺序是先尝试读寄存器,如果不存在则尝试读高速缓存。如果还不存在则读RAM,最后才是读ROM。一般CPU有三级cache,读取时是一级一级往下直到找到需要的操作数,做的比较好的CPU的3级缓存能让命中率达到95%以上。
Java 内存模型
有了上面的知识再往下探索就水到渠成了,如果把Java内存模型与多级存储机制类比我们能够发现Java为了提高性能而引入了工作内存的概念。可以把Java模型中的主存和工作内存分别与RAM和高速缓存或寄存器对应起来,每条线程的工作内存预先把需要的数据复制到高速缓存或寄存器(但是不保证所有的工作内存的变量副本都是放在高速缓存,也可能在RAM,具体的还要看JVM是如何实现的),这样就提高了线程执行时读取数据的速度,在多线程并发时性能得到保证。当然寄存器和高速缓存由于成本原因存在容量大小限制的问题,这个也是考验JVM实现的一个难题。
需要注意的是,JMM是个抽象的内存模型,所以所谓的本地内存,主内存都是抽象概念,并不一定就真实的对应cpu缓存和物理内存
数据同步问题
一般当我们引入一种机制解决了一个问题,同时也会带来另外一个问题。数据同步即是带来的另一个问题,即是否能保证当前运算使用的变量值总是当前时刻最新的值。如果变量值并非最新值,将会导致数据的脏读,最终可能导致计算结果大相径庭。这时可能有人会想起Java中有个volatile关键词,毫无疑问它能保证可见性,让每个线程得到的都是主存中最新的变量值,但它是否足以保证数据的同步性呢?
我们来看一个典型的例子,伪代码如下。执行完所有线程任务,我们期望的结果会是30*10000。但实际却是一个小于30*10000的数,刚开始看到一定觉得有点奇怪,但仔细一想就清楚了。count++;编译后最终并非一个原子操作,它由几个指令一起组合实现。在Java内存模型中,count++;被分割成多个步骤,这几步不具有原子性。假如在完成的过程中,其他线程就去读了主存的count变量,那明显将导致脏读现象。
volatile无锁
导致这个问题的原因其实是因为volatile不具备锁操作,要解决此问题其实不难,就是将这这些操作变为原子操作。即保证线程一完成之前不能有其他线程读取count变量,要达到目的只需对count变量加一个互斥锁即可。线程一执行前对count加锁,其他线程无法对count进行访问。线程一执行完后释放锁,此刻开始才允许其他线程获取此变量。
总结
Volatile是一个很容易搞混的关键词,很多经验丰富的开发人员都不能正确使用它。本文从机器结构讲到对应的Java内存模型,再引出主存与工作内存之间数据同步的问题。进而更好地解释了volatile的确切含义,它只保证可见性,它不足以保证数据的同步性。
Java 并发编程
基础知识
版权声明: 本文为 InfoQ 作者【码农架构】的原创文章。
原文链接:【http://xie.infoq.cn/article/1af7f384682354680bb33161c】。文章转载请联系作者。
评论