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

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

而在 JDK 8 里面 AtomicLong 里面的一些方法也是自旋,但是就不仅仅依赖于


cmpxchg 指令做了,比如还是上面这个方法:




可以看到这里面还是有一个 do-while 的循环,还是调用的 compareAndSwapLong 方法:


这个方法对应的 Lock 前缀指令是我们前面提到过的 xadd 指令。


从 Java 代码的角度来看,都是自旋,都是 compareAndSwapLong 方法。没有什么差异。


但是从这篇 oracle 官网的文章,我们可以窥见 JDK 8 在 x86 平台上对 compareAndSwapLong 方法做了一些操作,使用了 xadd 汇编指令代替 CAS 操作。


xadd 指令是 fetch and add。


cmpxchg 指令是 compare and swap。



xadd 指令的性能是优于 cmpxchg 指令的。


具体可以看看这篇 oracle 官网的文章:


https://blogs.oracle.com/dave/atomic-fetch-and-add-vs-compare-and-swap


文章下面的评论,可以多注意一下,我截取其中两个,大家品一品:


然后是这个:


总之就是:这篇文章说的有道理,我们(Dave and Doug)也在思考这个问题。所以我们会在 JIT 上面搞事情,在 x86 平台上把 CAS 操作替换为 LOCK:XADD 指令。


(这个地方我之前理解的有问题,经过朋友的指正后才修改过来。)


所以,JDK 8 之后的 AtomicLong 里面的方法都是经过改良后, xadd+cmpxchg 双重加持的方法。


另外需要注意的是,我怕有的朋友懵逼,专门多提一嘴:CAS 是指一次比较并交换的过程,成功了就返回 true,失败了则返回 false,强调的是一次。而自旋 CAS 是在死循环里面进行比较并交换,只要不返回 true 就一直循环。


所以,不要一提到 CAS 就说循环时间开销大。前面记得加上“自旋”和“竞争大”两个条件。


至于 JDK 8 使用 xadd 汇编指令代替 CAS 操作的是否真的是性能更好了,可以看看这篇 oracle 官网的文章:


https://blogs.oracle.com/dave/atomic-fetch-and-add-vs-compare-and-swap


文章下面的评论,可以多注意一下,我截取其中一个,大家品一品:


经过我们前面的分析,AtomicLong 从 JDK 7 到 JDK 8 是有一定程度上的性能优化的,但是改动并不大。


还是存在一个问题:虽然它可以实现原子性的增减操作,但是当竞争非常大的时候,被操作的这个 value 就是一个热点数据,所有线程都要去对其进行争抢,导致并发修改时冲突很大。


所以,归根到底它的主要问题还是出在共享热点数据上。


为了解决这个问题,Doug Lea 在 JDK 8 里面引入了 LongAdder 类。


更加牛逼的LongAdder



大家先看一下官网上的介绍:


上面的截图一共两段话,是对 LongAdder 的简介,我给大家翻译并解读一下。


首先第一段:当有多线程竞争的情况下,有个叫做变量集合(set of variables)的东西会动态的增加,以减少竞争。sum() 方法返回的是某个时刻的这些变量的总和。


所以,我们知道了它的返回值,不论是 sum() 方法还是 longValue() 方法,都是那个时刻的,不是一个准确的值。


意思就是你拿到这个值的那一刻,这个值其实已经变了。


这点是非常重要的,为什么会是这样呢?


我们对比一下 AtomicLong 和 LongAdder 的自增方法就可以知道了:


AtomicLong 的自增是有返回值的,就是一个这次调用之后的准确的值,这是一个原子性的操作。


LongAdder 的自增是没有返回值的,你要获取当前值的时候,只能调用 sum 方法。

你想这个操作:先自增,再获取值,这就不是原子操作了。


所以,当多线程并发调用的时候,sum 方法返回的值必定不是一个准确的值。除非你加锁。


该方法上的说明也是这样的:


至于为什么不能返回一个准确的值,这就是和它的设计相关了,这点放在后面去说。


然后第二段:当在多线程的情况下对一个共享数据进行更新(写)操作,比如实现一些统计信息类的需求,LongAdder 的表现比它的老大哥 AtomicLong 表现的更好。在并发不高的时候,两个类都差不多。但是高并发时 ngAdder 的吞吐量明显高一点,它也占用更多的空间。这是一种空间换时间的思想。


这段话其实是接着第一段话在进行描述的。


因为它在多线程并发情况下,没有一个准确的返回值,所以当你需要根据返回值去搞事情的时候,你就要仔细思考思考,这个返回值你是要精准的,还是大概的统计类的数据就行。


比如说,如果你是用来做序号生成器,所以你需要一个准确的返回值,那么还是用 AtomicLong 更加合适


如果你是用来做计数器,这种写多读少的场景。比如接口访问次数的统计类需求,不需要时时刻刻的返回一个准确的值,那就上 LongAdder 吧


总之,AtomicLong 是可以保证每次都有准确值,而 LongAdder 是可以保证最终数据是准确的。高并发的场景下 LongAdder 的写性能比 AtomicLong 高。


接下来探讨三个问题:


  • LongAdder 是怎么解决多线程操作热点 value 导致并发修改冲突很大这个问题的?


  • 为什么高并发场景下 LongAdder 的 sum 方法不能返回一个准确的值?


  • 为什么高并发场景下 LongAdder 的写性能比 AtomicLong 高?


先带大家看个图片,看不懂没有关系,先有个大概的印象:


接下来我们就去探索源码,源码之下无秘密。


从源码我们可以看到 add 方法是关键:


里面有 cells 、base 这样的变量,所以在解释 add 方法之前,我们先看一下 这几个成员变量。


这几个变量是 Striped64 里面的。


LongAdder 是 Striped64 的子类:


其中的四个变量如下:


  • NCPU:cpu 的个数,用来决定 cells 数组的大小。


  • cells:一个数组,当不为 null 的时候大小是 2 的次幂。里面放的是 cell 对象。


  • base : 基数值,当没有竞争的时候直接把值累加到 base 里面。还有一个作用就是在 cells 初始化时,由于 cells 只能初始化一次,所以其他竞争初始化操作失败线程会把值累加到 base 里面。


  • cellsBusy:当 cells 在扩容或者初始化的时候的锁标识。


之前,文档里面说的 set of variables 就是这里的 cells。

目录
相关文章
|
微服务
我从LongAdder中窥探到了高并发的秘籍,上面只写了两个字... (3)
我从LongAdder中窥探到了高并发的秘籍,上面只写了两个字... (3)
134 0
|
缓存 Java API
我从LongAdder中窥探到了高并发的秘籍,上面只写了两个字... (1)
我从LongAdder中窥探到了高并发的秘籍,上面只写了两个字... (1)
90 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