荒腔走板
大家好,我是why。
时间过的真是快,一周又要结束了。那么,你比上周更博学了吗?先来一个简短的荒腔走板,给冰冷的技术文注入一丝色彩。
上面这图是我之前拼的一副拼图,一共划分了800块,背面无提示,难度极高,我花了两周的时间才拼完。
拼的是坛城,传说中佛祖居住生活的地方。
第一次知道这个名词是 2015 年,窝在寝室看纪录片《第三极》。
其中有一个片段讲的就是僧人为了某个节日用沙绘画坛城,他们的那种专注,虔诚,真挚深深的打动了我,当宏伟的坛城画完之后,他静静的等待节日的到来。
本以为节日当天众人会对坛城顶礼膜拜,而实际情况是大家手握一炷香,看着众僧人快速的摧毁坛城。
还没来得及仔细欣赏那复杂的美丽的图案,却又用扫把扫的干干净净。
扫把扫下去的那一瞬间,我的心受到了一种强烈的撞击:可以辛苦地拿起,也可以轻松地放下。
看到摧毁坛城的片段的时候,有一个弹幕是这样说的:
一切有为法,如梦幻泡影,如露亦如电,应作如是观。
这句话出自《金刚般若波罗蜜经》第三十二品,应化非真分。
因为之前翻阅过几次《金刚经》,看到这句话的时候我一下就想起了它。
因为读的时候我就觉得这句话很有哲理,但是也似懂非懂。所以印象比较深刻。
当他再次在坛城这个画面上以弹幕的形式展现在我的眼前的时候,我一下就懂了其中的哲理,不敢说大彻大悟,至少领悟一二。
观看摧毁坛城,这个色彩斑斓的世界变幻消失的过程,正常人的感受都是震撼,转而觉得可惜,心里久久不能平静。
但是僧人却风轻云淡的说:一切有为法,如梦幻泡影,如露亦如电,应作如是观。
好了,说回文章。
先说AtomicLong
关于 AtomicLong 我就不进行详细的介绍了。
先写这一小节的目的是预热一下,抛出一个问题,而这个问题是关于 CAS 操作和 volatile 关键字的。
我不知道源码为什么这样写,希望知道答案的朋友指点一二。
抱拳了,老铁。
为了顺利的抛出这个问题,我就得先用《Java并发编程的艺术》一书做引子,引出这个问题。
首先在书的第 2.3 章节《原子操作的实现原理》中介绍处理器是如何实现原子操作时提到了两点:
- 使用总线锁保证原子性。
- 使用缓存锁保证原子性。
所谓总线锁就是使用处理器提供一个提供的一个 LOCK # 信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
总线锁保证原子性的操作有点简单粗暴直接了,导致总线锁定的开销比较大。
所以,目前处理器在某些场合下使用缓存锁来进行优化。
缓存锁的概念可以看一下书里面怎么写的:
其中提到的图 2-3 是这样的:
其实关键 Lock 前缀指令。
被 Lock 前缀指令操作的内存区域就会加锁,导致其他处理器不能同时访问。
而根据 IA-32 架构软件开发者手册可以知道,Lock 前缀的指令在多核处理器下会引发两件事情:
- 将当前处理器缓存行的数据写回系统内存。
- 这个写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效。
对于 volatile 关键字,毫无疑问,我们是知道它是使用了 Lock 前缀指令的。
那么问题来了,JVM 的 CAS 操作使用了 Lock 前缀指令吗?
是的,使用了。
JVM 中的 CAS 操作使用的是处理器通过的 CMPXCHG 指令实现的。这也是一个 Lock 前缀指令。
好,接下来我们看一个方法:
java.util.concurrent.locks.AbstractQueuedLongSynchronizer#compareAndSetState
这个方法位于 AQS 包里面,就是一个 CAS 的操作。现在只需要关心我框起来的部分。
英文部分翻译过来是:这个操作具有 volatile 读和写的内存语言。
而这个操作是什么操作?
就是 344 行 unsafe 的 compareAndSwapLong 操作,这个方法是一个 native 方法。
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
为什么这个操作具有 volatile 读和写的内存语言呢?
书里面是这样写的:
这个本地方法的最终实现在 openjdk 的如下位置: openjdk-7-fcs-src-b147- 27_jun_2011\openjdk\hotspot\src\os_cpu\windows_x86\vm\atomic_windows_x86.inline.hpp(对应于Windows操作系统,X86处理器)
intel 的手册对 Lock 前缀的说明如下。
- 确保对内存的读-改-写操作原子执行。在 Pentium 及 Pentium 之前的处理器中,带有 Lock 前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4、Intel Xeon及P6处理器开始,Intel使用缓存锁定(Cache Locking) 来保证指令执行的原子性。缓存锁定将大大降低lock前缀指令的执行开销。
- 禁止该指令,与之前和之后的读和写指令重排序。
- 把写缓冲区中的所有数据刷新到内存中。
上面的第2点和第3点所具有的内存屏障效果,足以同时实现 volatile 读和volatile 写的内存语义。
好,如果你说你对书上的内容存疑。那么我带大家再看看官方文档:
https://docs.oracle.com/javase/8/docs/api/
我框起来的部分:
compareAndSet 和所有其他的诸如 getAndIncrement 这种读然后更新的操作拥有和 volatile 读、写一样的内存语义。
原因就是用的到了 Lock 指令。
好,到这里我们可以得出结论了:
compareAndSet 同时具有volatile读和volatile写的内存语义。
那么问题就来了!
这个操作,在 AtomicLong 里面也有调用:
而 AtomicLong 里面的 value 又是被 volatile 修饰了的:
请问:为什么 compareAndSwapLong 操作已经同时具有 volatile 读和 volatile 写的内存语义了,其操作的 value 还需要被 volatile 修饰呢?
这个问题也是一个朋友抛出来探讨的,探讨的结果是,我们都不知道为什么:
我猜测会不会是由于操作系统不同而不同。在 x86 上面运行是这样,其他的操作系统就不一定了,但是没有证据。
希望知道为什么这样做的朋友能指点一下。
好,那么前面说到 CAS ,那么一个经典的面试题就来了:
请问,CAS 实现原子操作有哪些问题呢?
- ABA问题。
- 循环时间开销大。
- 只能保证一个共享变量的原子操作。
如果上面这三点你不知道,或者你说不明白,那我建议你看完本文后一定去了解一下,属于面试常问系列。
我主要说说这个循环时间开销大的问题。自旋 CAS 如果长时间不成功,就会对 CPU 带来比较大的执行开销。
而回答这个问题的朋友,大多数举例的时候都会说: “AtomicLong 就是基于自旋 CAS 做的,会带来一定的性能问题。巴拉巴拉......”
而我作为面试官的时候只是微笑着看着你,让你错以为自己答的很完美。
我知道你为什么这样答,因为你看了几篇博客,刷了刷常见面试题,那里面都是这样写的 :AtomicLong 就是基于自旋 CAS 做的。
但是,朋友,你可以这样说,但是回答不完美。这题得分别从 JDK 7 和 JDK 8 去答:
JDK 7 的 AtomicLong 是基于自旋 CAS 做的,比如下面这个方法:
while(true) 就是自旋,自旋里面纯粹依赖于 compareAndSet 方法:
这个方法里面调用的 native 的 comareAndSwapLong 方法,对应的 Lock 前缀指令就是我们前面说到的 cmpxchg。