精通并发编程无锁设计技巧 /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】。文章转载请联系作者。









评论