在 Java 多线程环境中,保证原子性的核心是让 "读取 - 修改 - 写入" 这类复合操作不可分割(要么全执行,要么全不执行)。除了之前提到的 synchronized 和 Lock,还有以下常用方式,涵盖 无锁方案、原子类、并发工具 等,附原理、场景和注意事项:
一、java.util.concurrent.atomic 原子类(无锁方案,首选)
Atomic 系列类是 JDK 1.5 引入的无锁并发工具,底层基于 CAS(Compare-And-Swap)操作 + volatile 实现原子性,无需加锁,性能优于 synchronized(高并发下无锁竞争开销)。
1. 核心原理
- CAS 操作:CPU 提供的原子指令,核心逻辑是 "比较内存值与预期值,若相等则更新为新值,否则失败",整个过程原子化。
- volatile 保证可见性 :原子类的变量被
volatile修饰,确保修改后立即写回主内存,其他线程能实时读取最新值。 - 自旋重试:若 CAS 操作失败,会循环重试直到成功(轻量级开销,适合低冲突场景)。
2. 常用原子类及场景
| 原子类 | 功能描述 | 适用场景 |
|---|---|---|
AtomicInteger/AtomicLong |
基本类型的原子增减、赋值操作 | 计数器、序号生成器(如订单 ID) |
AtomicBoolean |
布尔值的原子修改(true/false 切换) | 状态标记(如 "是否初始化完成") |
AtomicReference |
引用类型的原子赋值(支持泛型) | 原子更新对象引用(如单例对象) |
AtomicStampedReference |
带版本号的原子引用(解决 ABA 问题) | 避免 CAS 中的 ABA 漏洞(如链表操作) |
AtomicIntegerFieldUpdater |
原子更新对象的非 volatile 字段(反射实现) | 无需修改原有类,对字段进行原子操作 |
3. 示例代码
java
运行
java
import java.util.concurrent.atomic.AtomicInteger;
// 原子计数器:多线程并发自增,保证计数准确
public class AtomicCounter {
// 原子类变量(volatile + CAS 保证原子性)
private final AtomicInteger count = new AtomicInteger(0);
// 原子自增(替代 count++,避免非原子操作)
public void increment() {
count.incrementAndGet(); // 底层:CAS 自旋重试,直到成功
}
// 原子赋值(替代 count = x)
public void set(int value) {
count.set(value);
}
// 原子累加(替代 count += x)
public void add(int delta) {
count.addAndGet(delta);
}
public int getCount() {
return count.get();
}
}
4. 注意事项
- ABA 问题 :普通原子类(如
AtomicInteger)可能出现 "值被修改后又改回原值" 的 ABA 漏洞(例如线程 1 读取值为 A,线程 2 改为 B 再改回 A,线程 1 CAS 仍会成功)。解决方案:用AtomicStampedReference(带版本号,每次修改版本号递增,避免误判)。 - 高冲突场景性能下降 :若并发冲突频繁(如上千线程同时 CAS),自旋重试会消耗大量 CPU,此时需改用锁(
synchronized/Lock)。 - 不支持复合逻辑 :原子类仅支持单一操作(如
incrementAndGet()、addAndGet()),若需多个原子操作组合(如 "先自增再判断是否超过阈值"),仍需额外同步。
二、java.util.concurrent.locks.ReentrantLock + Condition(灵活锁方案)
ReentrantLock 是 Lock 接口的核心实现,通过 显式加锁 / 解锁 保证同步块内操作的原子性,功能比 synchronized 更灵活(支持超时、中断、公平锁),原子性保证逻辑与 synchronized 一致(互斥执行)。
1. 原理
- 同一时间只有一个线程能获取锁,同步块内的所有操作(包括复合操作)会被 "包裹" 为原子执行单元,其他线程需等待锁释放后才能进入。
- 支持 可重入:同一线程可多次获取锁(需对应释放,避免死锁)。
2. 示例代码(原子执行复合操作)
java
运行
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// 用 ReentrantLock 保证"自增+判断"的原子性
public class LockAtomicExample {
private final Lock lock = new ReentrantLock();
private int count = 0;
private static final int MAX_COUNT = 1000;
// 复合操作:自增后判断是否超过阈值(原子执行)
public boolean incrementAndCheck() {
lock.lock(); // 显式加锁
try {
count++; // 读取-修改-写入复合操作
return count > MAX_COUNT; // 原子性保证:自增和判断不可分割
} finally {
lock.unlock(); // 必须在 finally 释放锁,避免锁泄露
}
}
}
3. 适用场景
- 需 复合原子操作(如 "先修改再判断""多变量原子更新")。
- 高并发场景下需要 公平锁 (按请求顺序获取锁,避免线程饥饿)或 超时等待(避免死锁)。
4. 注意事项
- 必须在
finally块中释放锁(否则线程异常时会导致锁泄露,其他线程无法获取)。 - 公平锁性能较低(需维护等待队列),默认是非公平锁(优先唤醒正在自旋的线程,性能更优)。
三、java.util.concurrent.locks.StampedLock(读写分离锁,优化读多写少场景)
StampedLock 是 JDK 1.8 引入的新型锁,支持 乐观读、悲观读、写锁 三种模式,原子性保证依赖 "写锁的互斥性",适合 读多写少 的场景(读操作无锁竞争,性能远超 ReentrantLock)。
1. 核心原理
- 写锁互斥:获取写锁后,其他线程无法获取读锁或写锁,保证写操作(如修改共享变量)的原子性。
- 乐观读无锁:读操作时无需加锁,仅通过 "版本戳" 判断期间是否有写操作修改数据,若未修改则直接使用,若已修改则升级为悲观读锁(避免数据不一致)。
2. 示例代码(写操作原子性保证)
java
运行
csharp
import java.util.concurrent.locks.StampedLock;
public class StampedLockAtomicExample {
private final StampedLock lock = new StampedLock();
private int value = 0;
// 写操作:原子修改 value(获取写锁,互斥执行)
public void setValue(int newValue) {
long stamp = lock.writeLock(); // 获取写锁,返回版本戳
try {
value = newValue; // 原子操作(无其他线程干扰)
} finally {
lock.unlockWrite(stamp); // 释放写锁,需传入版本戳
}
}
// 读操作:乐观读(无锁),若被修改则升级为悲观读
public int getValue() {
long stamp = lock.tryOptimisticRead(); // 乐观读,无锁
int currentValue = value;
// 验证期间是否有写操作修改数据
if (!lock.validate(stamp)) {
stamp = lock.readLock(); // 升级为悲观读锁
try {
currentValue = value;
} finally {
lock.unlockRead(stamp);
}
}
return currentValue;
}
}
3. 适用场景
- 读操作远多于写操作(如缓存查询、数据统计),需保证写操作的原子性,同时提升读性能。
4. 注意事项
- 不支持可重入(同一线程多次获取写锁会死锁),需手动避免。
- 乐观读升级为悲观读时需注意性能开销,避免频繁升级。
四、ThreadLocal(线程隔离,避免共享变量竞争)
ThreadLocal 并非直接保证 "共享变量的原子性",而是通过 线程隔离 彻底避免共享 ------ 每个线程持有变量的独立副本,线程间互不干扰,自然无需考虑原子性问题。
1. 核心原理
ThreadLocal内部维护一个ThreadLocalMap(线程私有),每个线程通过ThreadLocal访问自己的副本变量,修改操作仅影响当前线程的副本,与其他线程无关。- 本质:"不共享即无竞争",间接实现 "线程内操作的原子性"(无需同步)。
2. 示例代码(线程隔离避免原子性问题)
java
运行
csharp
public class ThreadLocalExample {
// 每个线程持有独立的计数器副本
private final ThreadLocal<Integer> threadLocalCount = ThreadLocal.withInitial(() -> 0);
// 线程内自增:仅修改当前线程的副本,无并发竞争
public void increment() {
threadLocalCount.set(threadLocalCount.get() + 1);
}
public int getCount() {
return threadLocalCount.get();
}
public static void main(String[] args) {
ThreadLocalExample example = new ThreadLocalExample();
// 线程1:自增后输出 1
new Thread(() -> {
example.increment();
System.out.println(example.getCount()); // 1
}).start();
// 线程2:自增后输出 1(与线程1的副本独立)
new Thread(() -> {
example.increment();
System.out.println(example.getCount()); // 1
}).start();
}
}
3. 适用场景
- 变量无需线程间共享(如每个线程的上下文信息:用户 ID、请求 ID)。
- 避免频繁创建临时变量(如工具类中的线程私有缓存)。
4. 注意事项
- 内存泄漏风险 :若线程池复用线程(如 Tomcat 线程池),
ThreadLocal副本未手动移除(remove()),会导致副本变量长期占用内存。解决方案:在使用完后调用threadLocal.remove()清理。 - 不适合线程间数据共享:若需线程间传递数据,
ThreadLocal无效,需改用原子类或锁。
五、java.util.concurrent.Semaphore(信号量,控制并发访问数)
Semaphore 是 "并发访问控制器",通过控制 允许同时访问的线程数 间接保证原子性 ------ 当信号量为 1 时,等价于互斥锁(同一时间仅一个线程执行临界区操作),从而保证原子性。
1. 核心原理
- 信号量维护一个 "许可数",线程获取许可(
acquire())后才能执行操作,执行完释放许可(release())。 - 当许可数 = 1 时,成为 "互斥信号量",实现与
synchronized类似的互斥效果,保证临界区操作原子性。
2. 示例代码(信号量保证原子性)
java
运行
java
import java.util.concurrent.Semaphore;
public class SemaphoreAtomicExample {
// 信号量许可数 = 1,等价于互斥锁
private final Semaphore semaphore = new Semaphore(1);
private int count = 0;
// 原子自增:通过信号量控制仅一个线程执行
public void increment() throws InterruptedException {
semaphore.acquire(); // 获取许可(无许可则阻塞)
try {
count++; // 原子操作(互斥执行)
} finally {
semaphore.release(); // 释放许可(必须在 finally 中)
}
}
public int getCount() {
return count;
}
}
3. 适用场景
- 需限制并发访问数(如 "最多 3 个线程同时操作数据库"),同时保证单个线程操作的原子性。
- 比
synchronized灵活:支持动态调整许可数(如根据系统负载修改并发数)。
4. 注意事项
- 必须在
finally中释放许可,否则会导致许可泄露(其他线程永远无法获取许可)。 - 支持中断(
acquireInterruptibly())和超时等待(tryAcquire(timeout)),可避免死锁。
六、java.util.concurrent.locks.ReadWriteLock(读写锁,读多写少优化)
ReadWriteLock 是 "读写分离锁",核心是 "读锁共享,写锁互斥"------ 多个线程可同时获取读锁(无竞争),但写锁与读锁、写锁与写锁互斥,从而保证写操作的原子性,读操作的一致性。
1. 核心原理
- 写锁(WriteLock)互斥:获取写锁后,其他线程无法获取读锁或写锁,保证 "修改操作" 的原子性(如 "读取 - 修改 - 写入" 不可分割)。
- 读锁(ReadLock)共享:多个线程可同时获取读锁,提升读操作性能(适合读多写少场景)。
2. 示例代码(写操作原子性保证)
java
运行
java
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private int data = 0;
// 写操作:原子修改数据(互斥执行)
public void updateData(int newValue) {
writeLock.lock(); // 获取写锁,互斥
try {
data = newValue; // 原子操作(无其他线程干扰)
} finally {
writeLock.unlock();
}
}
// 读操作:共享获取数据(无竞争)
public int getData() {
readLock.lock();
try {
return data;
} finally {
readLock.unlock();
}
}
}
3. 适用场景
- 读操作远多于写操作(如缓存、配置中心),需保证写操作的原子性,同时提升读性能。
4. 注意事项
- 避免 "写锁饥饿":若读锁长期被占用,写锁可能一直无法获取(
ReentrantReadWriteLock默认非公平锁,可通过构造函数指定公平锁缓解)。 - 读锁不能升级为写锁:若线程已获取读锁,再尝试获取写锁会导致死锁(需先释放读锁)。
七、总结:不同原子性方案的选择建议
| 方案 | 核心优势 | 适用场景 | 性能对比(高并发) |
|---|---|---|---|
Atomic 原子类 |
无锁、高性能、API 简洁 | 单变量原子操作(计数器、状态标记) | 最优(无锁竞争) |
ReentrantLock |
灵活(超时、中断、公平锁)、支持复合操作 | 多变量复合原子操作、高冲突场景 | 中(锁竞争开销) |
StampedLock |
读多写少场景性能最优(乐观读无锁) | 读远多于写、需保证写操作原子性 | 读操作最优,写操作与 ReentrantLock 相当 |
ThreadLocal |
无并发竞争、线程隔离 | 变量无需共享(上下文、临时变量) | 无竞争,性能极高(但不共享) |
Semaphore(许可 = 1) |
支持动态调整并发数、灵活控制访问权限 | 需限制并发数的原子操作 | 中(信号量调度开销) |
ReadWriteLock |
读锁共享、写锁互斥,读多写少场景优化 | 读多写少、需保证写操作原子性 | 读操作最优,写操作互斥开销 |
核心选择原则:
- 单变量原子操作(如计数器):优先用
Atomic原子类(无锁,性能最好)。 - 多变量复合操作(如 "先修改再判断"):用
ReentrantLock或synchronized(互斥保证原子性)。 - 读多写少场景:用
StampedLock或ReadWriteLock(平衡读性能和写原子性)。 - 变量无需共享:用
ThreadLocal(线程隔离,无竞争)。 - 需限制并发数:用
Semaphore(许可 = 1 等价互斥锁,支持动态调整)。