JAVA并发计数器方案对比:AtomicLong、LongAdder、AdderCell深入剖析
大家好,今天我们来深入探讨Java并发计数器方案,重点对比AtomicLong、LongAdder以及AdderCell的实现原理、性能特点和适用场景。在多线程环境下,保证计数器的正确性和性能是一个常见且重要的挑战。选择合适的计数器方案,直接关系到应用程序的并发处理能力。
1. 并发计数器需求与挑战
在高并发场景下,多个线程同时对一个计数器进行读写操作,如果没有适当的同步机制,就会出现数据竞争,导致计数结果错误。 常见的需求包括:
- 原子性: 保证计数器操作的原子性,即一个线程的操作不会被其他线程中断。
- 可见性: 保证计数器值的可见性,即一个线程修改后的值,其他线程能够立即看到。
- 性能: 在高并发情况下,尽量减少锁的竞争,提高计数器的性能。
传统的解决方案是使用synchronized关键字或者ReentrantLock来保证原子性和可见性。但这些基于锁的机制在高并发下会造成线程阻塞和上下文切换,降低性能。
2. AtomicLong:简单的原子操作
AtomicLong是java.util.concurrent.atomic包下的一个类,它使用CAS(Compare and Swap)操作来实现原子性的long类型变量。
原理:
AtomicLong内部维护一个volatile修饰的long类型变量。volatile保证了变量的可见性。所有对AtomicLong的操作,如get()、set()、incrementAndGet()、decrementAndGet()等,都通过CAS操作来实现原子性。
CAS操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。CAS操作会原子性地将内存位置V的值与预期原值A进行比较,如果相等,那么将内存位置V的值更新为新值B,否则,说明期间有其他线程修改了该值,当前线程的操作失败。通常情况下,CAS操作会进行重试,直到成功为止。
代码示例:
import java.util.concurrent.atomic.AtomicLong;
public class AtomicLongCounter {
private AtomicLong counter = new AtomicLong(0);
public void increment() {
counter.incrementAndGet();
}
public long getCount() {
return counter.get();
}
public static void main(String[] args) throws InterruptedException {
AtomicLongCounter counter = new AtomicLongCounter();
int numThreads = 10;
int iterations = 1000;
Thread[] threads = new Thread[numThreads];
for (int i = 0; i < numThreads; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < iterations; j++) {
counter.increment();
}
});
threads[i].start();
}
for (int i = 0; i < numThreads; i++) {
threads[i].join();
}
System.out.println("Counter value: " + counter.getCount()); // Expected: 10000
}
}
优点:
- 实现简单,易于理解和使用。
- 在高并发量较低的情况下,性能较好。
缺点:
- 在高并发情况下,CAS操作失败的概率会增加,导致线程不断重试,造成CPU资源的浪费。这被称为自旋。
- 只有一个计数器,所有线程都竞争这一个资源,容易形成热点竞争。
3. LongAdder:分段累加,减少竞争
LongAdder是JDK 8中引入的,它通过将一个long类型的变量分解为多个Cell,每个Cell拥有独立的value,降低了单个变量的竞争。
原理:
LongAdder内部维护一个Cell数组。当多个线程并发更新计数器时,它们会被分配到不同的Cell上进行累加,从而减少了竞争。 最终,可以通过对所有Cell的value求和来得到最终的计数结果。
LongAdder的核心是Cell类,它本质上是一个带有@sun.misc.Contended注解的AtomicLong。@sun.misc.Contended注解用于防止伪共享。
伪共享: CPU缓存以缓存行为单位进行加载,通常一个缓存行包含多个变量。 如果多个线程访问的变量位于同一个缓存行中,即使这些变量之间没有逻辑上的依赖关系,也会因为一个线程修改了缓存行中的某个变量,导致其他线程的缓存行失效,需要重新加载,从而降低性能。 @sun.misc.Contended注解会尝试将每个Cell对象填充到不同的缓存行,避免伪共享。
代码示例:
import java.util.concurrent.atomic.LongAdder;
public class LongAdderCounter {
private LongAdder counter = new LongAdder();
public void increment() {
counter.increment();
}
public long getCount() {
return counter.sum();
}
public static void main(String[] args) throws InterruptedException {
LongAdderCounter counter = new LongAdderCounter();
int numThreads = 10;
int iterations = 1000;
Thread[] threads = new Thread[numThreads];
for (int i = 0; i < numThreads; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < iterations; j++) {
counter.increment();
}
});
threads[i].start();
}
for (int i = 0; i < numThreads; i++) {
threads[i].join();
}
System.out.println("Counter value: " + counter.getCount()); // Expected: 10000
}
}
优点:
- 在高并发情况下,性能优于
AtomicLong,因为减少了锁的竞争。 - 通过分段累加,降低了热点竞争。
- 防止伪共享,提高CPU缓存利用率。
缺点:
- 空间占用较大,因为需要维护一个Cell数组。
sum()操作需要在所有Cell上进行累加,如果Cell数量很多,可能会有一定的性能损耗。- 在线程数较少的情况下,性能可能不如
AtomicLong。
4. AdderCell:LongAdder的核心组件
AdderCell并非直接暴露给开发者使用的类,而是LongAdder内部使用的核心组件。 它本身就是一个AtomicLong,但使用了@sun.misc.Contended注解来防止伪共享。
原理:
AdderCell的设计目标是减少并发冲突。当多个线程尝试更新同一个AdderCell时,会使用CAS操作。如果CAS操作失败,说明发生了冲突,线程会尝试更新其他AdderCell。
代码示例 (模拟 AdderCell 的使用):
虽然我们不能直接创建和使用AdderCell,但可以通过模拟其核心逻辑来理解它的工作方式:
import java.util.concurrent.atomic.AtomicLong;
public class SimulatedAdderCell {
// 使用 @sun.misc.Contended 注解,需要添加 JVM 参数 -XX:-RestrictContended
// 实际使用时,需要引入 sun.misc.Contended 包。这里为了演示方便,省略注解。
//@sun.misc.Contended
private AtomicLong value = new AtomicLong(0);
public long get() {
return value.get();
}
public boolean cas(long expectedValue, long newValue) {
return value.compareAndSet(expectedValue, newValue);
}
public long incrementAndGet() {
return value.incrementAndGet();
}
public static void main(String[] args) throws InterruptedException {
SimulatedAdderCell cell = new SimulatedAdderCell();
int numThreads = 10;
int iterations = 1000;
Thread[] threads = new Thread[numThreads];
for (int i = 0; i < numThreads; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < iterations; j++) {
cell.incrementAndGet();
}
});
threads[i].start();
}
for (int i = 0; i < numThreads; i++) {
threads[i].join();
}
System.out.println("Cell value: " + cell.get()); // Expected: 10000
}
}
关键点:
AdderCell是LongAdder的基础,LongAdder通过维护一个AdderCell数组来实现分段累加。AdderCell内部使用AtomicLong来保证原子性。@sun.misc.Contended注解用于防止伪共享。
5. 性能对比与选择建议
为了更直观地了解AtomicLong和LongAdder的性能差异,可以进行基准测试。以下是一个简单的基准测试代码:
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
public class CounterBenchmark {
private static final int NUM_THREADS = 10;
private static final int ITERATIONS = 1000000;
public static void main(String[] args) throws InterruptedException {
System.out.println("Starting benchmark...");
// AtomicLong benchmark
AtomicLong atomicLongCounter = new AtomicLong(0);
long atomicLongTime = benchmarkAtomicLong(atomicLongCounter);
System.out.println("AtomicLong time: " + atomicLongTime + " ms");
System.out.println("AtomicLong value: " + atomicLongCounter.get());
// LongAdder benchmark
LongAdder longAdderCounter = new LongAdder();
long longAdderTime = benchmarkLongAdder(longAdderCounter);
System.out.println("LongAdder time: " + longAdderTime + " ms");
System.out.println("LongAdder value: " + longAdderCounter.sum());
System.out.println("Benchmark finished.");
}
private static long benchmarkAtomicLong(AtomicLong counter) throws InterruptedException {
Thread[] threads = new Thread[NUM_THREADS];
long startTime = System.currentTimeMillis();
for (int i = 0; i < NUM_THREADS; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < ITERATIONS; j++) {
counter.incrementAndGet();
}
});
threads[i].start();
}
for (int i = 0; i < NUM_THREADS; i++) {
threads[i].join();
}
long endTime = System.currentTimeMillis();
return endTime - startTime;
}
private static long benchmarkLongAdder(LongAdder counter) throws InterruptedException {
Thread[] threads = new Thread[NUM_THREADS];
long startTime = System.currentTimeMillis();
for (int i = 0; i < NUM_THREADS; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < ITERATIONS; j++) {
counter.increment();
}
});
threads[i].start();
}
for (int i = 0; i < NUM_THREADS; i++) {
threads[i].join();
}
long endTime = System.currentTimeMillis();
return endTime - startTime;
}
}
基准测试结果分析 (仅供参考,实际结果取决于硬件环境和JVM配置):
| 计数器类型 | 线程数 | 迭代次数 | 耗时 (ms) |
|---|---|---|---|
| AtomicLong | 10 | 1000000 | 约 500 |
| LongAdder | 10 | 1000000 | 约 200 |
选择建议:
| 场景 | 计数器选择 | 理由 |
|---|---|---|
| 低并发,读多写少 | AtomicLong | 实现简单,性能足够满足需求。 |
| 高并发,写多读少 | LongAdder | 通过分段累加,减少了锁的竞争,性能更优。 |
| 计数结果实时性要求高 | AtomicLong | AtomicLong能保证读取到的值是最新值,而LongAdder的sum()操作可能存在一定的延迟。 |
| 内存资源紧张 | AtomicLong | AtomicLong占用空间更小。 |
| 对计数器的统计信息(例如最大值、最小值)有复杂的需求 | 自定义方案 | 可以基于LongAdder进行扩展,例如维护一个最大值和一个最小值,并在每次更新时进行比较和更新。也可以选择其他更复杂的并发数据结构,例如ConcurrentHashMap。 |
6. LongAccumulator:更灵活的累加器
LongAccumulator是JDK 8中引入的另一个类,它比LongAdder更加灵活。LongAccumulator允许你自定义累加的逻辑,而LongAdder只能进行加法操作。
原理:
LongAccumulator接受一个LongBinaryOperator函数作为参数,用于定义累加的逻辑。LongBinaryOperator是一个函数式接口,它接受两个long类型的参数,并返回一个long类型的结果。
代码示例:
import java.util.concurrent.atomic.LongAccumulator;
import java.util.function.LongBinaryOperator;
public class LongAccumulatorExample {
public static void main(String[] args) throws InterruptedException {
// 计算最大值
LongBinaryOperator maxOp = Math::max;
LongAccumulator maxAccumulator = new LongAccumulator(maxOp, Long.MIN_VALUE);
// 模拟多线程更新
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
long value = (long) (Math.random() * 1000);
maxAccumulator.accumulate(value);
}
});
threads[i].start();
}
for (int i = 0; i < 10; i++) {
threads[i].join();
}
System.out.println("Max value: " + maxAccumulator.get());
// 计算最小值
LongBinaryOperator minOp = Math::min;
LongAccumulator minAccumulator = new LongAccumulator(minOp, Long.MAX_VALUE);
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
long value = (long) (Math.random() * 1000);
minAccumulator.accumulate(value);
}
});
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
long value = (long) (Math.random() * 1000);
minAccumulator.accumulate(value);
}
});
threads[i].start();
}
for (int i = 0; i < 10; i++) {
threads[i].join();
}
System.out.println("Min value: " + minAccumulator.get());
}
}
优点:
- 可以自定义累加逻辑,更加灵活。
- 在高并发情况下,性能优于直接使用
synchronized或ReentrantLock。
缺点:
- 实现相对复杂,需要理解
LongBinaryOperator函数式接口。 - 如果累加逻辑比较复杂,可能会影响性能。
7. 总结
AtomicLong、LongAdder和LongAccumulator都是Java并发包中用于实现并发计数器的类。AtomicLong实现简单,适用于低并发场景;LongAdder通过分段累加减少竞争,适用于高并发的累加场景;LongAccumulator提供了更灵活的累加逻辑定制,可用于计算最大值、最小值等更复杂的统计信息。在选择计数器方案时,需要根据具体的应用场景和性能需求进行权衡。合理地使用这些类,可以有效地提高并发应用程序的性能和可靠性。