多线程环境下对共享变量进行计数累加,最直观的写法是用一个 int 或 long 字段,然后多个线程各自对它执行 count++ 或 count += delta。这种代码在低并发时可能碰巧跑不出问题,一旦并发上来,累加结果几乎必然偏小。根本原因在于 count++ 并不是一个原子操作,它至少包含“读取-修改-写入”三个独立步骤,线程切换恰好发生在中间,就会造成更新丢失。
要解决这个问题,最容易想到的是加锁,比如用 synchronized 块或者显式的 Lock 把整个累加操作串行化。加锁确实能保证正确性,但在高并发计数场景下,线程会频繁阻塞和唤醒,上下文切换的开销会急剧放大,吞吐量很快掉下来。对于计数器这类极度轻量的操作,锁的代价往往比业务逻辑本身还大。
Java 在 java.util.concurrent.atomic 包里提供了一组 Atomic 类,专门用来在无锁条件下完成单个变量的原子更新。它们依赖的是底层硬件提供的 CAS(Compare-And-Swap)指令,由 CPU 保证比较并交换的原子性,不需要操作系统层面的线程挂起。用 Atomic 类实现计数累加,既能避免加锁的阻塞,又能保证结果的正确性,是目前多线程计数场景下性价比最高的方案之一。
AtomicInteger 和 AtomicLong 的基本用法AtomicInteger 和 AtomicLong 分别封装了一个 volatile 的 int 和 long 值,并暴露出一系列原子更新方法。最常用的累加方法是 incrementAndGet()、getAndIncrement()、addAndGet(delta) 和 getAndAdd(delta)。
假设你需要统计某个服务处理的总请求数,可以这样写:
import java.util.concurrent.atomic.AtomicLong;
public class RequestCounter {
private final AtomicLong counter = new AtomicLong(0);
public void increment() {
counter.incrementAndGet();
}
public long getTotal() {
return counter.get();
}
}
incrementAndGet() 内部会不断尝试用 CAS 把当前值加一,如果发现值已被其他线程改过,就重新读取最新值再试,直到成功。这个“自旋”过程在竞争不极端的情况下非常快,通常只有几个 CPU 周期。因为没有任何线程进入阻塞状态,也就避免了上下文切换的昂贵开销。
addAndGet(delta) 的逻辑完全一样,只是把加一的逻辑换成加 delta。如果你需要一次累加多个计数,比如批量处理了一组请求后一次性更新,用 addAndGet 比多次调用 incrementAndGet 效率更高,因为减少了 CAS 的重试次数。
为什么 volatile 不够用很多人知道 volatile 能保证可见性,就认为给 count 字段加上 volatile,然后直接 count++ 就能解决并发问题。实际上 volatile 只保证每次读取都能看到最新写入的值,禁止指令重排序,但 count++ 的“读取-修改-写入”复合操作整体仍然不是原子的。线程 A 读取了最新值 5,还没来得及写回,线程 B 也读到 5,两个线程都计算 5+1=6 并分别写回,最终结果是 6 而不是 7。Atomic 类通过 CAS 把“读取-比较-修改-写入”这四个步骤打包成一个不可分割的硬件操作,从根本上解决了这个问题。
LongAdder 和 DoubleAdder:高竞争下的更优解AtomicLong 在中等并发下表现很好,但当线程数非常多、竞争极度激烈时,大量线程同时 CAS 同一个内存位置,会导致大量重试,CPU 空转严重,性能会出现明显退化。Java 8 引入的 LongAdder 就是专门为这种高竞争计数场景设计的。
LongAdder 的核心思想是分散热点。它内部维护了一个 base 值和一个 Cell 数组。当竞争较低时,直接 CAS 更新 base;一旦检测到竞争加剧,就把计数分散到多个 Cell 上,每个线程大概率会操作不同的 Cell,从而大幅降低冲突概率。获取总值时,sum() 方法会把 base 和所有 Cell 的值累加起来返回。
用法也很简单:
import java.util.concurrent.atomic.LongAdder;
public class HighThroughputCounter {
private final LongAdder counter = new LongAdder();
public void increment() {
counter.increment();
}
public long getTotal() {
return counter.sum();
}
}
LongAdder 的 increment() 和 add(x) 方法没有返回值,这是为了减少不必要的操作开销。如果你需要累加后立即获取新值,LongAdder 不适合,此时应该继续用 AtomicLong。但绝大多数计数场景都是先疯狂累加,最后才读取汇总值,LongAdder 恰好匹配这种模式。
同理,DoubleAdder 是为 double 类型设计的类似实现,适用于高并发下的浮点累加。
AtomicIntegerFieldUpdater 和 AtomicLongFieldUpdater:零额外内存开销AtomicInteger 和 AtomicLong 本身是对象,每个实例都会占用一定的堆内存。如果你有大量对象需要各自的计数器,比如每个用户会话、每个连接都需要计数,为每个对象都创建一个 AtomicLong 字段会显著增加内存压力。这时可以用 AtomicLongFieldUpdater,它利用反射机制,把某个类的 volatile long 字段“升级”成具备原子更新能力,而不需要额外创建对象。
示例代码如下:
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
public class Session {
private volatile long requestCount = 0;
private static final AtomicLongFieldUpdater UPDATER =
AtomicLongFieldUpdater.newUpdater(Session.class, "requestCount");
public void incrementRequestCount() {
UPDATER.incrementAndGet(this);
}
public long getRequestCount() {
return requestCount;
}
}
注意被升级的字段必须声明为 volatile,否则 Updater 无法保证可见性。这种方式的缺点是反射会带来极轻微的性能开销,并且字段名是字符串,编译期无法检查。但在内存敏感且对象实例量巨大的场景下,这是非常实用的技巧。
CAS 的 ABA 问题及其在计数场景下的实际影响CAS 操作有一个经典的 ABA 问题:一个值原来是 A,被改成 B,后来又改回 A,CAS 检测时发现值还是 A,就认为没有被修改过,实际上中间已经发生过变化。在计数累加场景下,ABA 问题几乎不会造成实际危害,因为计数器的值本来就是单调递增或递减的,你关心的是“当前值是多少”,而不是“中间有没有人改过又改回来”。只要最终累加的结果正确,中间是否经历过 ABA 并不影响业务正确性。所以用 Atomic 类做计数时,完全不需要额外处理 ABA 问题,这和用 AtomicReference 实现无锁栈等数据结构的情况完全不同。
实际性能对比与选型建议在低到中等并发(通常不超过 CPU 核心数的 2-3 倍线程数)下,AtomicLong 和 LongAdder 的吞吐量差距不大,AtomicLong 甚至因为结构简单,延迟可能更低。一旦线程数显著超过核心数,或者竞争集中在极短时间窗口内,LongAdder 的吞吐量可以比 AtomicLong 高出数倍甚至一个数量级。
具体选型可以这样判断:
如果你的计数器更新频率很高、线程数很多,且读取汇总值的频率远低于更新频率,直接用 LongAdder 或 DoubleAdder,这是高吞吐场景的最优解。
如果你需要在每次更新后立即获取最新值,比如生成递增序列号,必须用 AtomicLong 或 AtomicInteger,因为 LongAdder 的 increment 方法没有返回值。
如果内存敏感且对象实例量极大,考虑用 FieldUpdater 把现有 volatile 字段升级,避免额外对象开销。
如果只是简单的多线程计数,没有极端性能要求,AtomicInteger 和 AtomicLong 足够简单可靠,代码可读性也最好。
用 JMH 验证不同方案的真实表现口说无凭,用 JMH 做一个微基准测试可以直观看到差异。以下是一个简化的测试框架,分别测试 synchronized、AtomicLong 和 LongAdder 在不同线程数下的吞吐量:
@BenchmarkMode(Mode.Througput)
@OututTimeUnit(TieUnit.SECONDS)
@State(Sope.Benchmak)
public class CounterBenchmark {
private long syncCount = 0;
private final AtomicLong atomicCount = new AtomicLong();
private final LongAdder adderCount = new LongAdder();
@Benchmark
public synchronized void synchrnizedIncrement() {
syncCount++;
}
@Benchmark
public void atomicIncrement() {
atomicCount.incrementAndGet();
}
@Benchmark
public void adderIncrement() {
adderCount.increment();
}
}
典型结果会显示:1 个线程时三者差距不大;4 个线程时 AtomicLong 明显优于 synchronized;16 个线程以上时 LongAdder 开始大幅领先 AtomicLong,synchronized 的吞吐量则基本停滞甚至下降。这个趋势充分说明了锁、Atomic 和分散热点三种策略在不同竞争强度下的适用边界。
使用中容易踩的坑第一,不要把 Atomic 类的 get() 和后续的 compareAndSet 分开使用,这破坏了原子性。如果你需要“先检查再更新”,应该直接用 compareAndSet 或 updateAndGet 等方法,它们在内部循环完成“读取-判断-更新”的原子操作。
第二,LongAdder 的 sum() 方法并不是一个快照,它遍历 Cell 数组的过程中,其他线程可能还在并发修改,所以 sum() 返回的是一个近似值,不是精确的瞬时值。在需要精确瞬时值的场景下,这点必须接受或用其他方案替代。
第三,不要在高竞争下频繁调用 LongAdder 的 sum()。sum() 的计算成本与 Cell 数组大小成正比,高竞争下 Cell 数量会动态膨胀,频繁调用 sum() 会拖累整体性能。最佳实践是尽量降低 sum() 的调用频率,比如只在日志输出或监控采集时调用一次。
第四,Atomic 类虽然是无锁的,但并非零开销。高竞争下 CAS 自旋会消耗 CPU 资源,如果你的业务逻辑本身就比较重,把计数更新和业务逻辑放在同一个线程里,竞争激烈时会把业务线程拖慢。此时可以考虑把计数异步化,比如用队列把计数请求批量交给一个专门的计数线程处理,不过这已经超出 Atomic 类本身的讨论范围了。
总结Java Atomic 类在多线程计数场景下提供了从简单到高性能的完整方案。AtomicInteger 和 AtomicLong 适合大多数通用场景,实现简洁,延迟低;LongAdder 和 DoubleAdder 在竞争激烈时通过分散热点大幅提升吞吐;FieldUpdater 则解决了内存敏感的细粒度计数需求。理解它们的内部机制和适用边界,能让你在设计高并发系统时,用最小的代价拿到正确的计数结果,而不必动不动就上重锁或引入外部中间件。
