在现代并发编程中,高效且线程安全的数据操作是关键。Striped64、AtomicLong和LongAdder是Java提供的核心工具,用于在多线程环境下进行精确且高效的数值操作。AtomicLong适用于单个long值的原子操作,而Striped64则通过分段技术优化高并发场景下的累加性能。LongAdder进一步扩展了这一概念,通过分散操作到多个Cell,显著降低了锁竞争,特别适合于高并发计数场景。这些工具不仅提高了性能,还简化了并发编程的复杂性,是构建高性能多线程应用的基石。
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 类的一个简化实现,展示了核心原理和代码结构。这个实现包括了单元值的更新、线程特定单元更新、检查竞争、扩展单元数组和累加到基础值的过程:
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、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在高并发更新场景下通常能提供更好的性能。
- 相比于使用
- 累加操作:
- 需要对数值进行累加操作,尤其是在统计和聚合操作中。
使用示例
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无锁原理
- 当前值 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 在低并发或需要精确控制的场景下更为合适。选择使用哪个类需要根据具体的业务场景和性能要求来决定。