写点什么

精通并发编程无锁设计技巧 /Striped64 设计借鉴

作者:肖哥弹架构
  • 2024-11-10
    河北
  • 本文字数:5028 字

    阅读完需:约 16 分钟

精通并发编程无锁设计技巧/Striped64设计借鉴


在现代并发编程中,高效且线程安全的数据操作是关键。Striped64AtomicLongLongAdder是 Java 提供的核心工具,用于在多线程环境下进行精确且高效的数值操作。AtomicLong适用于单个long值的原子操作,而Striped64则通过分段技术优化高并发场景下的累加性能。LongAdder进一步扩展了这一概念,通过分散操作到多个Cell,显著降低了锁竞争,特别适合于高并发计数场景。这些工具不仅提高了性能,还简化了并发编程的复杂性,是构建高性能多线程应用的基石。


肖哥弹架构 跟大家“弹弹” 高并发锁, 关注公号回复 'mvcc' 获得手写数据库事务代码

欢迎 点赞,关注,评论。

关注公号 Solomon 肖哥弹架构获取更多精彩内容

历史热点文章

1、Striped64 介绍

Striped64 是 Java 并发包 java.util.concurrent.atomic 中的一个核心组件,它提供了一种高效的方式来进行数值的累加操作,特别是在高并发环境下。以下是 Striped64 算法的工作原理和特点:


  1. 分散计算Striped64 的核心思想是分散计算,将对一个共享变量的累加操作分散到多个单元(Cell)中,从而减少竞争和锁的争用。每个线程都会尝试更新自己的单元,而不是直接更新共享变量 base

  2. 基于 Hash 的分配Striped64 利用线程的 threadLocalRandomProbe 值作为哈希值,将不同的线程分配到 cells 数组的不同位置,实现线程的均匀分布和负载均衡。

  3. 懒初始化cells 数组是懒初始化的,只有在需要时(即当 base 更新失败,表明存在竞争时)才会创建。这样可以避免不必要的内存分配和初始化开销。

  4. 动态扩容: 当 cells 数组中的某个位置竞争激烈时,Striped64 会动态地扩展 cells 数组的大小,以进一步分散竞争。数组的大小通常不会超过 CPU 核心数,以保持高效的分散计算。

  5. 无锁或轻量级锁Striped64 使用 cellsBusy 作为轻量级的锁标志,用于控制对 cells 数组的修改操作。这种设计减少了锁的开销,提高了性能。

  6. 最终累加: 当需要获取累加结果时,Striped64 会将 base 和所有 cells 中的值累加在一起,提供最终的累计结果。

  7. 避免伪共享Striped64 使用 @Contended 注解来避免 CPU 缓存行的伪共享问题,进一步提高了性能。

2、Striped64 类设计图


类图有以下组件:


  • Striped64

  • cells:一个 Cell 类型的数组,用于存储多个累加单元,实现并发累加。

  • base:一个 long 类型的变量,用于存储累加结果的基础值。

  • cellsBusy:一个 int 类型的变量,用作自旋锁,保护对 cells 数组的修改。

  • longAccumulate:一个方法,用于累加一个 long 值。

  • longAdd:一个方法,用于添加一个 long 值。

  • Cell

  • value:一个 long 类型的变量,存储单元的值。

  • cas:一个方法,使用 CAS 操作来原子地更新单元的值。

3、Striped64 核心原理


  1. 线程本地随机探针:每个线程使用自己的本地随机探针值作为哈希值,用于确定在单元数组中的位置。

  2. 单元数组Striped64 维护一个单元数组,用于存储每个线程的累加值。

  3. 基础值Striped64 还维护一个基础值,用于存储所有单元的累加结果。

  4. 单元值:每个线程更新自己对应的单元值。

  5. 线程特定单元更新:线程更新自己的单元值,而不是直接更新基础值。

  6. 检查竞争:检查是否存在竞争,即多个线程尝试更新同一个单元。

  7. 扩展单元数组:如果存在竞争,扩展单元数组的大小,以减少竞争。

  8. 累加到基础值:在读取最终值时,将所有单元值累加到基础值中。

  9. 读取最终值:读取基础值和所有单元值的累加结果,作为最终的累计值。

3.1 高并发处理核心实现代码

以下是 Striped64 类的一个简化实现,展示了核心原理和代码结构。这个实现包括了单元值的更新、线程特定单元更新、检查竞争、扩展单元数组和累加到基础值的过程:


import java.util.concurrent.atomic.AtomicInteger;import java.util.concurrent.atomic.AtomicLong;
// Striped64 类用于提供线程安全的 long 类型数值累加操作。public class Striped64 { // 基础值,用于存储累加结果。 private final AtomicLong base = new AtomicLong(0);
// 单元数组,每个单元是一个 AtomicLong 实例,用于分散线程的更新操作。 private final AtomicLong[] cells;
// 用于控制单元数组修改的标记,防止多线程同时扩展数组。 private final AtomicInteger cellsBusy = new AtomicInteger(0);
// 构造函数,根据并行级别创建合适大小的单元数组。 public Striped64(int parallelism) { int size = 1; while (size < parallelism) { size <<= 1; // 将大小翻倍,直到至少与并行级别一样大。 } cells = new AtomicLong[size]; for (int i = 0; i < cells.length; ++i) { cells[i] = new AtomicLong(0); // 初始化每个单元为 0。 } }
// 线程安全的累加方法。 public void increment() { int index = ThreadLocalRandom.current().nextInt(); // 获取线程特定的随机索引。 AtomicLong[] cs = cells; if (cs[index].incrementAndGet() == 0) { // 如果单元值增加后为 0,说明之前有其他线程已经处理过这个单元。 retryUpdate(index); // 重新尝试更新。 } }
// 重新尝试更新单元值或扩展单元数组。 private void retryUpdate(int index) { AtomicLong[] cs = cells; boolean uncontended = true; // 标记是否无竞争。 while (true) { int c = cellsBusy.get(); // 获取当前 cellsBusy 的值。 if (c >= 0 && uncontended) { // 如果 cellsBusy 为非负数且当前无竞争。 if (cellsBusy.compareAndSet(c, -1)) { // 尝试将 cellsBusy 设置为 -1,表示正在更新。 uncontended = false; // 标记竞争状态。 } } else if (c == Integer.MAX_VALUE) { // 如果 cellsBusy 为 Integer.MAX_VALUE,表示正在扩展数组。 int n = cs.length; // 获取当前数组长度。 AtomicLong[] rs = new AtomicLong[n << 1]; // 创建新的数组,大小翻倍。 for (int i = 0; i < n; ++i) { // 复制旧数组到新数组。 rs[i] = new AtomicLong(cs[i].get()); } cells = rs; // 更新 cells 数组引用。 cellsBusy.set(0); // 重置 cellsBusy。 return; // 返回并重新尝试更新。 } else if (uncontended) { // 如果当前无竞争。 uncontended = false; // 标记竞争状态。 cellsBusy.set(c + 1); // 增加 cellsBusy 的值。 } long v = cs[index].get(); // 获取当前单元的值。 if (v == 0 && cs[index].compareAndSet(v, 1)) { // 如果单元值为 0 并且成功设置为 1。 break; // 退出循环。 } } }
// 获取累加结果。 public long get() { long sum = base.get(); // 获取基础值。 AtomicLong[] cs = cells; // 获取单元数组。 for (int i = 0; i < cs.length; ++i) { // 遍历单元数组。 sum += cs[i].get(); // 将每个单元的值累加到总和中。 } return sum; // 返回总和。 }}
复制代码

4、LongAdderDoubleAdder

LongAdderDoubleAdder 是 Java 中提供的两个用于并发编程的类,它们属于 java.util.concurrent.atomic 包。这些类的主要作用是提供一种高效的方式来进行数值的累加操作,特别是在面对高并发场景时,能够减少因竞争导致的性能问题。

LongAdder

LongAdder 是一个用于原子地更新 long 值的类,它利用 Striped64 算法来减少多个线程更新同一变量时的争用。这个类适用于场景中,多个线程需要频繁地对同一个 long 值进行增加操作,但又希望避免使用同步锁带来的性能开销。

DoubleAdder

DoubleAdderLongAdder 类似,但它是用于 double 类型的数值。由于 double 值不是原始类型,DoubleAdder 内部使用 long 类型的两个字段来分别存储 double 值的高 32 位和低 32 位,从而实现原子更新。

适用场景

  1. 高并发计数

  2. 在高并发系统中,多个线程可能需要对同一个数值进行频繁的增加操作,如统计事件的发生次数、用户访问量等。

  3. 避免锁竞争

  4. 在多线程环境中,传统的同步方法(如 synchronizedReentrantLock)可能会导致锁竞争,影响性能。LongAdderDoubleAdder 提供了一种无锁的解决方案。

  5. 性能优化

  6. 相比于使用 AtomicLongAtomicIntegerLongAdderDoubleAdder 在高并发更新场景下通常能提供更好的性能。

  7. 累加操作

  8. 需要对数值进行累加操作,尤其是在统计和聚合操作中。

使用示例

import java.util.concurrent.atomic.LongAdder;
public class LongAdderExample { // 定义一个全局的 LongAdder 对象用于线程安全的计数 private static final LongAdder counter = new LongAdder();
public static void main(String[] args) { // 创建并启动 10 个线程,每个线程执行 1000 次计数操作 for (int i = 0; i < 10; i++) { new Thread(() -> { for (int j = 0; j < 1000; j++) { // 线程安全的增加计数 counter.add(1); } }).start(); }
// 等待所有线程完成。这里使用 Thread.activeCount() > 1 来检查是否还有其他线程在运行。 // 这是一个简单的等待策略,实际应用中可能需要更稳健的等待机制。 while (Thread.activeCount() > 1) { Thread.yield(); // 让出当前线程的执行权,以便其他线程可以运行 }
// 输出最终的总计数结果 System.out.println("Total count: " + counter.sum()); }}
复制代码

5、AtomicLong 无锁原理


  1. 当前值 V:获取 AtomicLong 当前的值。

  2. 期望值 A == V? :检查提供的期望值 A 是否等于当前值 V

  3. :如果期望值等于当前值,说明没有其他线程修改过这个值。

  4. 更新值 W:将新值 W 更新到 AtomicLong

  5. :如果期望值不等于当前值,说明有其他线程已经修改过这个值,需要重新尝试 CAS 操作。

  6. 新值 W 成功写入:如果更新成功,那么 CAS 操作完成。

6、AtomicLong 与 LongAdder 对比

AtomicLongLongAdder 都是 Java 中用于进行线程安全的数值操作的工具,但它们在设计和适用场景上有所不同。以下是两者的对比:


  1. 基本原理

  2. AtomicLong:基于 CAS(Compare-And-Swap)算法实现,保证单个 long 值的原子操作。

  3. LongAdder:基于分段的思想,内部维护一个 Cell 数组,将对单一变量的更新压力分散到多个 Cell 上,从而减少竞争和提高性能。

  4. 适用场景

  5. AtomicLong:适用于低并发或中等并发场景,以及需要精确控制中间状态的场景。

  6. LongAdder:适用于高并发场景,尤其是写操作远多于读操作的场景,如统计和计数。

  7. 性能特点

  8. AtomicLong:在低并发环境下性能表现良好,但在高并发环境下可能会遇到性能瓶颈,因为多个线程竞争同一个变量。

  9. LongAdder:在高并发环境下性能更优,因为它通过分散更新压力到多个 Cell 上来减少竞争。

  10. 内存占用

  11. AtomicLong:占用固定的内存空间,与线程数量无关。

  12. LongAdder:可能会占用更多的内存,因为它维护了一个 Cell 数组来适应高并发场景。

  13. 结果准确性

  14. AtomicLong:提供精确的即时值,适用于需要获取任意时刻精确值的场景。

  15. LongAddersum() 方法返回的值可能不是最新的,因为它需要遍历所有 Cell 来计算总和,这期间可能会有新的更新。

  16. API 丰富性

  17. AtomicLong:提供了更丰富的 API,如 compareAndSetgetAndAdd 等。

  18. LongAdder:API 相对简单,主要提供 addsum 方法。


总结来说,LongAdder 在高并发写入场景下性能更优,而 AtomicLong 在低并发或需要精确控制的场景下更为合适。选择使用哪个类需要根据具体的业务场景和性能要求来决定。

发布于: 刚刚阅读数: 3
用户头像

智慧属心窍之锁 2019-05-27 加入

擅长于通信协议、微服务架构、框架设计、消息队列、服务治理、PAAS、SAAS、ACE\ACP、大模型

评论

发布
暂无评论
精通并发编程无锁设计技巧/Striped64设计借鉴_Java_肖哥弹架构_InfoQ写作社区