精通并发编程无锁设计技巧 /Striped64 设计借鉴
在现代并发编程中,高效且线程安全的数据操作是关键。Striped64
、AtomicLong
和LongAdder
是 Java 提供的核心工具,用于在多线程环境下进行精确且高效的数值操作。AtomicLong
适用于单个long
值的原子操作,而Striped64
则通过分段技术优化高并发场景下的累加性能。LongAdder
进一步扩展了这一概念,通过分散操作到多个Cell
,显著降低了锁竞争,特别适合于高并发计数场景。这些工具不仅提高了性能,还简化了并发编程的复杂性,是构建高性能多线程应用的基石。
肖哥弹架构 跟大家“弹弹” 高并发锁, 关注公号回复 'mvcc' 获得手写数据库事务代码
欢迎 点赞,关注,评论。
关注公号 Solomon 肖哥弹架构获取更多精彩内容
历史热点文章
1、Striped64 介绍
Striped64
是 Java 并发包 java.util.concurrent.atomic
中的一个核心组件,它提供了一种高效的方式来进行数值的累加操作,特别是在高并发环境下。以下是 Striped64
算法的工作原理和特点:
分散计算:
Striped64
的核心思想是分散计算,将对一个共享变量的累加操作分散到多个单元(Cell
)中,从而减少竞争和锁的争用。每个线程都会尝试更新自己的单元,而不是直接更新共享变量base
。基于 Hash 的分配:
Striped64
利用线程的threadLocalRandomProbe
值作为哈希值,将不同的线程分配到cells
数组的不同位置,实现线程的均匀分布和负载均衡。懒初始化:
cells
数组是懒初始化的,只有在需要时(即当base
更新失败,表明存在竞争时)才会创建。这样可以避免不必要的内存分配和初始化开销。动态扩容: 当
cells
数组中的某个位置竞争激烈时,Striped64
会动态地扩展cells
数组的大小,以进一步分散竞争。数组的大小通常不会超过 CPU 核心数,以保持高效的分散计算。无锁或轻量级锁:
Striped64
使用cellsBusy
作为轻量级的锁标志,用于控制对cells
数组的修改操作。这种设计减少了锁的开销,提高了性能。最终累加: 当需要获取累加结果时,
Striped64
会将base
和所有cells
中的值累加在一起,提供最终的累计结果。避免伪共享:
Striped64
使用@Contended
注解来避免 CPU 缓存行的伪共享问题,进一步提高了性能。
2、Striped64 类设计图
类图有以下组件:
Striped64:
cells
:一个Cell
类型的数组,用于存储多个累加单元,实现并发累加。base
:一个long
类型的变量,用于存储累加结果的基础值。cellsBusy
:一个int
类型的变量,用作自旋锁,保护对cells
数组的修改。longAccumulate
:一个方法,用于累加一个long
值。longAdd
:一个方法,用于添加一个long
值。Cell:
value
:一个long
类型的变量,存储单元的值。cas
:一个方法,使用 CAS 操作来原子地更新单元的值。
3、Striped64 核心原理
线程本地随机探针:每个线程使用自己的本地随机探针值作为哈希值,用于确定在单元数组中的位置。
单元数组:
Striped64
维护一个单元数组,用于存储每个线程的累加值。基础值:
Striped64
还维护一个基础值,用于存储所有单元的累加结果。单元值:每个线程更新自己对应的单元值。
线程特定单元更新:线程更新自己的单元值,而不是直接更新基础值。
检查竞争:检查是否存在竞争,即多个线程尝试更新同一个单元。
扩展单元数组:如果存在竞争,扩展单元数组的大小,以减少竞争。
累加到基础值:在读取最终值时,将所有单元值累加到基础值中。
读取最终值:读取基础值和所有单元值的累加结果,作为最终的累计值。
3.1 高并发处理核心实现代码
以下是 Striped64
类的一个简化实现,展示了核心原理和代码结构。这个实现包括了单元值的更新、线程特定单元更新、检查竞争、扩展单元数组和累加到基础值的过程:
4、LongAdder
和 DoubleAdder
LongAdder
和 DoubleAdder
是 Java 中提供的两个用于并发编程的类,它们属于 java.util.concurrent.atomic
包。这些类的主要作用是提供一种高效的方式来进行数值的累加操作,特别是在面对高并发场景时,能够减少因竞争导致的性能问题。
LongAdder
LongAdder
是一个用于原子地更新 long
值的类,它利用 Striped64
算法来减少多个线程更新同一变量时的争用。这个类适用于场景中,多个线程需要频繁地对同一个 long
值进行增加操作,但又希望避免使用同步锁带来的性能开销。
DoubleAdder
DoubleAdder
与 LongAdder
类似,但它是用于 double
类型的数值。由于 double
值不是原始类型,DoubleAdder
内部使用 long
类型的两个字段来分别存储 double
值的高 32 位和低 32 位,从而实现原子更新。
适用场景
高并发计数:
在高并发系统中,多个线程可能需要对同一个数值进行频繁的增加操作,如统计事件的发生次数、用户访问量等。
避免锁竞争:
在多线程环境中,传统的同步方法(如
synchronized
或ReentrantLock
)可能会导致锁竞争,影响性能。LongAdder
和DoubleAdder
提供了一种无锁的解决方案。性能优化:
相比于使用
AtomicLong
或AtomicInteger
,LongAdder
和DoubleAdder
在高并发更新场景下通常能提供更好的性能。累加操作:
需要对数值进行累加操作,尤其是在统计和聚合操作中。
使用示例
5、AtomicLong 无锁原理
当前值 V:获取
AtomicLong
当前的值。期望值 A == V? :检查提供的期望值
A
是否等于当前值V
。是:如果期望值等于当前值,说明没有其他线程修改过这个值。
更新值 W:将新值
W
更新到AtomicLong
。否:如果期望值不等于当前值,说明有其他线程已经修改过这个值,需要重新尝试 CAS 操作。
新值 W 成功写入:如果更新成功,那么 CAS 操作完成。
6、AtomicLong 与 LongAdder 对比
AtomicLong
和 LongAdder
都是 Java 中用于进行线程安全的数值操作的工具,但它们在设计和适用场景上有所不同。以下是两者的对比:
基本原理:
AtomicLong
:基于 CAS(Compare-And-Swap)算法实现,保证单个long
值的原子操作。LongAdder
:基于分段的思想,内部维护一个Cell
数组,将对单一变量的更新压力分散到多个Cell
上,从而减少竞争和提高性能。适用场景:
AtomicLong
:适用于低并发或中等并发场景,以及需要精确控制中间状态的场景。LongAdder
:适用于高并发场景,尤其是写操作远多于读操作的场景,如统计和计数。性能特点:
AtomicLong
:在低并发环境下性能表现良好,但在高并发环境下可能会遇到性能瓶颈,因为多个线程竞争同一个变量。LongAdder
:在高并发环境下性能更优,因为它通过分散更新压力到多个Cell
上来减少竞争。内存占用:
AtomicLong
:占用固定的内存空间,与线程数量无关。LongAdder
:可能会占用更多的内存,因为它维护了一个Cell
数组来适应高并发场景。结果准确性:
AtomicLong
:提供精确的即时值,适用于需要获取任意时刻精确值的场景。LongAdder
:sum()
方法返回的值可能不是最新的,因为它需要遍历所有Cell
来计算总和,这期间可能会有新的更新。API 丰富性:
AtomicLong
:提供了更丰富的 API,如compareAndSet
、getAndAdd
等。LongAdder
:API 相对简单,主要提供add
和sum
方法。
总结来说,LongAdder
在高并发写入场景下性能更优,而 AtomicLong
在低并发或需要精确控制的场景下更为合适。选择使用哪个类需要根据具体的业务场景和性能要求来决定。
版权声明: 本文为 InfoQ 作者【肖哥弹架构】的原创文章。
原文链接:【http://xie.infoq.cn/article/f899bad98c554d2e2a893c395】。文章转载请联系作者。
评论