在多线程编程场景中,多个线程同时修改同一个共享计数器是常见需求,传统的synchronized加锁方案虽然能保证线程安全,但会带来上下文切换、阻塞等待等性能开销。而atomic原子类基于硬件层面的CAS(Compare And Swap)机制实现,可以在不加锁的情况下保证计数操作的原子性,是更高效的解决方案。

atomic的核心原理
atomic类的操作依赖CPU提供的原子指令,以Java中的AtomicInteger为例,它的累加操作并不是简单的读取-修改-写入三步,而是通过CAS机制完成:先读取当前值,计算目标值,然后比较当前值是否和之前读取的一致,如果一致就把目标值写入,不一致就重试整个流程,直到成功为止。整个过程不需要阻塞线程,也不会出现数据竞争问题。
加锁方案与atomic方案对比
我们可以通过一个简单的对比来看两种方案的区别:
| 方案类型 | 线程安全保证方式 | 性能表现 | 适用场景 |
|---|---|---|---|
| synchronized加锁 | 互斥锁,同一时间只有一个线程能操作计数器 | 有锁竞争时性能较低 | 操作逻辑复杂、需要多步原子操作的场景 |
| atomic原子类 | CAS硬件原子指令,无锁重试 | 无锁竞争时性能更高 | 简单的单变量原子操作场景 |
代码示例:使用atomic实现安全计数器
以下是Java中使用AtomicInteger实现多线程安全累加的示例:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
// 初始化原子计数器,初始值为0
private static final AtomicInteger counter = new AtomicInteger(0);
// 计数器累加方法,每次加1
public static void increment() {
counter.incrementAndGet();
}
// 获取当前计数器值
public static int getCount() {
return counter.get();
}
public static void main(String[] args) throws InterruptedException {
// 创建10个线程,每个线程累加1000次
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
increment();
}
});
}
// 启动所有线程
for (Thread thread : threads) {
thread.start();
}
// 等待所有线程执行完成
for (Thread thread : threads) {
thread.join();
}
// 输出最终计数器结果,预期为10000
System.out.println("最终计数器值:" + getCount());
}
}
运行上述代码,最终输出的结果一定是10000,不会出现少加的情况,说明AtomicInteger确实保证了累加操作的线程安全。
使用atomic的注意事项
- atomic只保证单个变量的操作是原子的,如果需要多个变量保持原子性,还是需要加锁或者使用
AtomicReference封装对象。 - 高竞争场景下,CAS重试次数过多会导致CPU占用升高,此时可以评估是否需要切换到加锁方案。
- 不同编程语言的atomic实现略有差异,比如C++的
std::atomic<int>、Go的sync/atomic包,核心原理都是CAS,但API使用方式不同,需要根据对应语言的规范使用。
其他语言的atomic计数器示例
以下是Go语言中使用sync/atomic实现计数器的示例:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var counter int64
var wg sync.WaitGroup
// 启动10个协程,每个协程累加1000次
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
// 原子累加操作
atomic.AddInt64(&counter, 1)
}
}()
}
wg.Wait()
// 输出最终计数器值,预期为10000
fmt.Println("最终计数器值:", counter)
}