我从LongAdder中窥探到了高并发的秘籍,上面只写了两个字... (1)

简介: 我从LongAdder中窥探到了高并发的秘籍,上面只写了两个字... (1)

荒腔走板


大家好,我是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。

目录
相关文章
|
微服务
我从LongAdder中窥探到了高并发的秘籍,上面只写了两个字... (3)
我从LongAdder中窥探到了高并发的秘籍,上面只写了两个字... (3)
134 0
|
Oracle Java 关系型数据库
我从LongAdder中窥探到了高并发的秘籍,上面只写了两个字... (2)
我从LongAdder中窥探到了高并发的秘籍,上面只写了两个字... (2)
116 0
|
5月前
|
消息中间件 Java Linux
2024年最全BATJ真题突击:Java基础+JVM+分布式高并发+网络编程+Linux(1),2024年最新意外的惊喜
2024年最全BATJ真题突击:Java基础+JVM+分布式高并发+网络编程+Linux(1),2024年最新意外的惊喜
|
5月前
|
Java
在高并发环境下,再次认识java 锁
在高并发环境下,再次认识java 锁
66 0
|
5月前
|
消息中间件 NoSQL Java
Java高级开发:高并发+分布式+高性能+Spring全家桶+性能优化
Java高架构师、分布式架构、高可扩展、高性能、高并发、性能优化、Spring boot、Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分布式项目实战学习架构师之路
|
4月前
|
缓存 NoSQL Java
Java高并发实战:利用线程池和Redis实现高效数据入库
Java高并发实战:利用线程池和Redis实现高效数据入库
456 0
|
4月前
|
存储 NoSQL Java
探索Java分布式锁:在高并发环境下的同步访问实现与优化
【6月更文挑战第30天】Java分布式锁在高并发下确保数据一致性,通过Redis的SETNX、ZooKeeper的临时节点、数据库操作等方式实现。优化策略包括锁超时重试、续期、公平性及性能提升,关键在于平衡同步与效率,适应大规模分布式系统的需求。
131 1
|
3月前
|
算法 Java 调度
高并发架构设计三大利器:缓存、限流和降级问题之使用Java代码实现令牌桶算法问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之使用Java代码实现令牌桶算法问题如何解决
|
3月前
|
监控 网络协议 Java
Java面试题:解释Java NIO与BIO的区别,以及NIO的优势和应用场景。如何在高并发应用中实现NIO?
Java面试题:解释Java NIO与BIO的区别,以及NIO的优势和应用场景。如何在高并发应用中实现NIO?
61 0
|
3月前
|
设计模式 安全 NoSQL
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
56 0