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

176 阅读10分钟

image.png

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

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 类设计图

image.png 类图有以下组件:

  • Striped64
    • cells:一个 Cell 类型的数组,用于存储多个累加单元,实现并发累加。
    • base:一个 long 类型的变量,用于存储累加结果的基础值。
    • cellsBusy:一个 int 类型的变量,用作自旋锁,保护对 cells 数组的修改。
    • longAccumulate:一个方法,用于累加一个 long 值。
    • longAdd:一个方法,用于添加一个 long 值。
  • Cell
    • value:一个 long 类型的变量,存储单元的值。
    • cas:一个方法,使用 CAS 操作来原子地更新单元的值。

3、Striped64 核心原理

image.png

  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. 避免锁竞争
    • 在多线程环境中,传统的同步方法(如 synchronized 或 ReentrantLock)可能会导致锁竞争,影响性能。LongAdder 和 DoubleAdder 提供了一种无锁的解决方案。
  3. 性能优化
    • 相比于使用 AtomicLong 或 AtomicIntegerLongAdder 和 DoubleAdder 在高并发更新场景下通常能提供更好的性能。
  4. 累加操作
    • 需要对数值进行累加操作,尤其是在统计和聚合操作中。

使用示例

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无锁原理

image.png

  1. 当前值 V:获取 AtomicLong 当前的值。
  2. 期望值 A == V? :检查提供的期望值 A 是否等于当前值 V
  3. :如果期望值等于当前值,说明没有其他线程修改过这个值。
  4. 更新值 W:将新值 W 更新到 AtomicLong
  5. :如果期望值不等于当前值,说明有其他线程已经修改过这个值,需要重新尝试 CAS 操作。
  6. 新值 W 成功写入:如果更新成功,那么 CAS 操作完成。

6、AtomicLong与LongAdder对比

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

  1. 基本原理
    • AtomicLong:基于 CAS(Compare-And-Swap)算法实现,保证单个 long 值的原子操作。
    • LongAdder:基于分段的思想,内部维护一个 Cell 数组,将对单一变量的更新压力分散到多个 Cell 上,从而减少竞争和提高性能。
  2. 适用场景
    • AtomicLong:适用于低并发或中等并发场景,以及需要精确控制中间状态的场景。
    • LongAdder:适用于高并发场景,尤其是写操作远多于读操作的场景,如统计和计数。
  3. 性能特点
    • AtomicLong:在低并发环境下性能表现良好,但在高并发环境下可能会遇到性能瓶颈,因为多个线程竞争同一个变量。
    • LongAdder:在高并发环境下性能更优,因为它通过分散更新压力到多个 Cell 上来减少竞争。
  4. 内存占用
    • AtomicLong:占用固定的内存空间,与线程数量无关。
    • LongAdder:可能会占用更多的内存,因为它维护了一个 Cell 数组来适应高并发场景。
  5. 结果准确性
    • AtomicLong:提供精确的即时值,适用于需要获取任意时刻精确值的场景。
    • LongAddersum() 方法返回的值可能不是最新的,因为它需要遍历所有 Cell 来计算总和,这期间可能会有新的更新。
  6. API丰富性
    • AtomicLong:提供了更丰富的 API,如 compareAndSetgetAndAdd 等。
    • LongAdder:API 相对简单,主要提供 add 和 sum 方法。

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