Java并发编程的艺术——读书笔记(二) volatile的应用

第二章 Java并发机制的底层实现原理(一)

这一章有三个内容,volatile的应用,synchronized的实现原理与应用,和原子操作的实现原理,我想分开说,这次先写volatile的相关内容。

volatile实现原理

保证可见性,不能保证原子性

首先提一下缓存,一般来说,对于存储设备,相同的造价,存储空间和读写速度是呈反相关的,因此存储设备会根据需求来设计偏向,通俗一点说就是,在计算机中,机械硬盘主要提供大容量的存储,因此读写性能较低,内存主要提供性能,因此容量相对于硬盘较小,但速度更快一些,CPU只与内存进行数据和指令的交互,内存中没有被需要的数据时会加载硬盘中的数据。此外还有多级缓存,读写速度更快,但容量也更小,一般只有几M,甚至只有多少K,缓存里面存储的是计算机中最常用的数据,这里有一个命中率的概念,计算机在缓存中读写数据的次数占总次数的比率越大,缓存命中率就越高

这就引出另一个问题了,缓存里的数据何时写入到主存中去?如果每次更新都写入主存(全写法),那么缓存的意义也就失去了大半,如果不能及时写入主存(写回法:该行缓存数据换出时写入),那么其他的进程/线程使用时就有可能得不到最新的数据,针对这个问题解决的方法是写一次法,是上面两种方法的折中,不过我的目的引入这个概念,对这个方法也不太了解,因此先不做探讨。

那么返回来看volatile的实现原理就简单多了,每个线程也是有各自的缓存的,用volatile修饰的变量,经某线程修改后会立即由缓存写入主存,同时通知其他线程缓存中的变量失效,强制让它们下次使用时从主存中重新读取,从而保证了变量的状态在所有线程中是唯一的,也就是保证了可见性。

Java语言规范第3版中对volatile的定义如下:

Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存 模型确保所有线程看到这个变量的值是一致的。

volatile的实现过程

首先将java代码转为汇编代码,可知volatile修饰的变量进行写操作的时候,后面会多一行lock开头的汇编代码。

这个lock前缀指令就是用来实现volatile的功能的,具体分两步:

lock指令发出信号,锁住缓存,并将其写回内存,保证修改的原子性;

处理器使用MESI控制协议去维护内部缓存和其他处理器缓存的一致性,如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。

volatile的使用优化

著名的Java并发编程大师Doug lea在JDK 7的并发包里新增一个队列集合类LinkedTransferQueue,它在使用volatile变量时,用一种追加字节的方式来优化队列出队和入队的性能。追加到64字节,能提高并发编程的效率,因为对于一些处理器来说,一个缓存行是64个字节宽,这样如果队列头节点和尾节点都不足64字节的话,处理器会将其读取到一个缓存行里,当处理器试图修改头节点时,lock指令会锁定整个缓存行,这会严重影响到入队和出队的效率,因此追加到64字节以保证头节点和尾节点不在一个缓存行内,就能提高并发效率。

但是这对于某些缓存行为32字节的处理器就不适用,而且如果共享变量不会被频繁的修改,这样的优化也带不来多大的收益。

 

volatile不保证原子性

说到底,volatile毕竟不是一个锁,以下一个例子能证明其不具有原子性。

volatile int test = 1;
test++;

第二行代码在处理过程中会分成三步,读取test-->修改test的值为原test+1-->存入test,得证。

猜你喜欢

转载自blog.csdn.net/qq_42734874/article/details/81126397