深入理解 Java 并发编程中的原子性:从原理到实战
深入理解 Java 并发编程中的原子性:从原理到实战
原子性是多线程编程中最基本也最容易被忽视的问题之一。本文将从 Java 内存模型出发,深入剖析原子性的本质、CAS 底层实现、原子类的使用,以及 AQS 等高级同步机制,帮助你构建完整的并发编程知识体系。
目录
- 一、什么是原子性?
- 二、Java 内存模型(JMM)与原子性
- 三、volatile 关键字——可见性与有序性
- 四、synchronized 关键字——互斥锁实现原子性
- 五、CAS 操作——无锁并发的基石
- 六、Java 原子类全解析
- 七、AQS 队列同步器
- 八、JVM 内存区域与并发安全
- 九、实战对比与最佳实践
- 十、总结
一、什么是原子性?
原子性(Atomicity) 是指一个操作或一组操作,要么全部执行且在执行过程中不被任何因素中断,要么全部不执行。在多线程环境下,原子性保证了共享变量的读写操作不会被其他线程干扰。
1.1 为什么需要原子性?
在单线程环境中,所有操作天然是顺序执行的,不存在原子性问题。但在多线程环境中,多个线程同时访问和修改共享变量时,就会出现竞态条件(Race Condition)。
来看一个经典例子:
public class Counter {
private int count = 0;
public void increment() {
count++; // 这行代码是原子操作吗?
}
public int getCount() {
return count;
}
}
看似简单的 count++,在字节码层面实际上包含四条指令(三个步骤):
步骤一:读取
getfield // 从主内存读取 count 的值
步骤二:计算
iconst_1 // 将常量 1 压入操作数栈
iadd // 执行加法
步骤三:写回
putfield // 将结果写回主内存
这意味着 count++ 不是一个原子操作。当两个线程同时执行 increment() 时,可能出现以下情况:
线程A: 读取 count = 0
线程B: 读取 count = 0
线程A: 计算 0 + 1 = 1
线程B: 计算 0 + 1 = 1
线程A: 写回 count = 1
线程B: 写回 count = 1 // 期望是 2,实际是 1!
这就是典型的**丢失更新(Lost Update)**问题。
1.2 Java 中的原子性保障层级
| 层级 | 机制 | 适用场景 |
|---|---|---|
| 基本类型读写 | JVM 规范保障(除 long/double 外) | 单个变量的简单读写 |
| volatile 变量 | 禁止重排序 + 内存可见性 | 单个变量的读写(不保证复合操作) |
| synchronized / Lock | 互斥锁 | 代码块/方法的原子性 |
| Atomic 原子类 | CAS 无锁算法 | 单个变量的原子更新 |
| AQS | 抽象队列同步器 | 构建自定义锁和同步工具 |
二、Java 内存模型(JMM)与原子性
2.1 什么是 Java 内存模型?
Java 内存模型(Java Memory Model,JMM) 是 Java 虚拟机规范中定义的一套规则,它规定了多线程程序中各个线程访问共享数据时的行为规范。JMM 的核心目标是解决并发编程中的三个关键问题:
- 可见性(Visibility):一个线程修改了共享变量,其他线程能够立刻看到修改后的值
- 有序性(Ordering):程序的执行顺序按照代码的先后顺序
- 原子性(Atomicity):一个或多个操作不被中断
2.2 JMM 的抽象结构
JMM 规定每个线程都有自己的工作内存(Working Memory),所有共享变量存储在**主内存(Main Memory)**中。
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Thread 1 │ │ Thread 2 │ │ Thread 3 │
│ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │
│ │工作内存 │ │ │ │工作内存 │ │ │ │工作内存 │ │
│ └────┬────┘ │ │ └────┬────┘ │ │ └────┬────┘ │
└──────┼──────┘ └──────┼──────┘ └──────┼──────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────┐
│ 主内存 │
│ (所有共享变量存储在此) │
└──────────────────────────────────────────────────────┘
线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
2.3 happens-before 原则
JMM 通过 happens-before 关系来保证内存操作的有序性和可见性。如果操作 A happens-before 操作 B,那么 A 的执行结果对 B 是可见的,且 A 的执行顺序排在 B 之前。
关键 happens-before 规则包括:
- 程序顺序规则:同一个线程中的操作,前面的操作 happens-before 后面的操作
- volatile 变量规则:对一个 volatile 变量的写操作,happens-before 后续对这个变量的读操作
- 锁规则:对一个锁的解锁操作,happens-before 后续对这个锁的加锁操作
- 线程启动规则:Thread.start() 操作 happens-before 该线程中的所有操作
- 线程终止规则:线程中的所有操作 happens-before 对该线程的 Thread.join() 的成功返回
- 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C
三、volatile 关键字——可见性与有序性
3.1 volatile 的两大特性
volatile 关键字是 Java 提供的一种轻量级同步机制,它提供了以下保证:
- 可见性:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个 volatile 变量,其他线程能立刻看到修改后的值
- 有序性(禁止指令重排序):编译器和处理器不会对 volatile 变量相关的指令进行重排序
但 volatile 不保证原子性,这一点非常关键。
3.2 volatile 的底层实现
volatile 的底层是通过**内存屏障(Memory Barrier)**来实现的:
- 对 volatile 变量的写操作,会在写指令前插入 StoreStore 屏障,在写指令后插入 StoreLoad 屏障
- 对 volatile 变量的读操作,会在读指令前插入 LoadLoad 屏障,在读指令后插入 LoadStore 屏障
写 volatile 变量:
StoreStore屏障
volatile 写操作
StoreLoad屏障
读 volatile 变量:
volatile 读操作
LoadLoad屏障
LoadStore屏障
3.3 volatile 不保证原子性的证明
public class VolatileAtomicTest {
private volatile int count = 0;
public void increment() {
count++; // volatile 不保证复合操作的原子性!
}
public static void main(String[] args) throws InterruptedException {
VolatileAtomicTest test = new VolatileAtomicTest();
int threadCount = 10;
int incrementsPerThread = 1000;
Thread[] threads = new Thread[threadCount];
for (int i = 0; i < threadCount; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < incrementsPerThread; j++) {
test.increment();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
// 期望输出 10000,但实际输出通常小于 10000
System.out.println("Final count: " + test.count);
}
}
原因分析:count++ 是一个复合操作(读-改-写),volatile 只保证了每次读和每次写本身的可见性,但没有保证这三个步骤作为一个整体不被打断。
3.4 volatile 的典型使用场景
虽然 volatile 不保证复合操作的原子性,但它在以下场景中非常有用:
场景一:状态标志位
volatile boolean shutdownRequested;
public void shutdown() {
shutdownRequested = true;
}
public void doWork() {
while (!shutdownRequested) {
// 执行业务逻辑
}
}
场景二:双重检查锁定(DCL)单例模式
public class Singleton {
// volatile 防止指令重排序
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // volatile 防止此处重排序
}
}
}
return instance;
}
}
为什么需要 volatile?因为
new Singleton()实际上分为三步:(1) 分配内存空间 (2) 初始化对象 (3) 将引用指向内存地址。如果没有 volatile,编译器可能将步骤 (2) 和 (3) 重排序,导致其他线程拿到一个未初始化的对象。
四、synchronized 关键字——互斥锁实现原子性
4.1 synchronized 的基本用法
synchronized 是 Java 中最基本的互斥同步手段,它可以修饰方法或代码块:
// 修饰实例方法——锁的是当前实例对象
public synchronized void method() {
// ...
}
// 修饰静态方法——锁的是当前类的 Class 对象
public static synchronized void method() {
// ...
}
// 修饰代码块——锁的是指定对象
public void method() {
synchronized (lockObject) {
// ...
}
}
4.2 synchronized 的实现原理
synchronized 在 JVM 层面基于**监视器(Monitor)**实现。每个 Java 对象都有一个与之关联的 Monitor。
进入 synchronized 代码块时,JVM 会执行 monitorenter 指令;退出时执行 monitorexit 指令。
synchronized(obj) {
// 同步代码块
}
// 等价于:
monitorenter // 尝试获取 obj 的 Monitor
// 同步代码块
monitorexit // 释放 obj 的 Monitor
monitorexit // 异常时的兜底释放
4.3 锁的优化(JDK 1.6+)
JDK 1.6 对 synchronized 进行了大量优化,引入了以下概念:
| 锁状态 | 说明 | 适用场景 |
|---|---|---|
| 偏向锁 | 假设只有一个线程访问,在对象头记录线程 ID | 几乎没有竞争 |
| 轻量级锁 | 竞争时升级为轻量级锁,使用 CAS 操作 | 少量线程短时间竞争 |
| 重量级锁 | 竞争激烈时升级为重量级锁,依赖 OS 互斥量 | 高并发长时间竞争 |
锁升级过程(不可降级):
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
重要更新(JDK 15+):由于偏向锁在实际应用中维护成本高且收益有限,JDK 15 默认禁用偏向锁(
-XX:-UseBiasedLocking),JDK 18 起正式移除偏向锁相关代码。在当前主流 JDK(17 LTS、21 LTS、25 LTS)中,锁升级路径为:无锁 → 轻量级锁 → 重量级锁
4.4 使用 synchronized 修复计数器问题
public class SafeCounter {
private int count = 0;
// 方式一:修饰方法
public synchronized void increment() {
count++;
}
// 方式二:修饰代码块(更灵活)
public void incrementWithBlock() {
synchronized (this) {
count++;
}
}
public synchronized int getCount() {
return count;
}
}
4.5 synchronized 的局限性
- 性能开销:虽然经过优化,但在高并发场景下仍有性能损耗
- 编码复杂:需要手动管理锁的获取和释放,容易遗漏
- 死锁风险:多把锁的获取顺序不当可能导致死锁
- 不可中断:线程等待获取锁时无法被中断
五、CAS 操作——无锁并发的基石
5.1 CAS 的基本原理
CAS(Compare And Swap,比较并交换) 是一种基于乐观锁的无锁算法。它可以在不加锁的情况下实现并发控制,是 Java 原子类的底层实现基础。
CAS 操作包含三个操作数:
| 操作数 | 含义 |
|---|---|
| V(Value) | 内存中的当前值(内存地址) |
| A(Expected) | 旧的预期值 |
| B(New) | 要设置的新值 |
CAS 的执行逻辑:只有当 V 的值等于 A 时,才将 V 的值更新为 B;否则不更新,返回当前 V 的值。
CAS(V, A, B) {
if (V == A) {
V = B;
return true;
} else {
return false;
}
}
5.2 CAS 的底层实现
CAS 在 x86 架构下通过 cmpxchg 指令实现,在 ARM 架构下通过 LL/SC(Load-Link / Store-Conditional)指令对实现。
Java 中的 CAS 操作主要通过 sun.misc.Unsafe 类来执行:
// Unsafe 类中的 CAS 方法(以 AtomicInteger 为例)
public final native boolean compareAndSwapInt(
Object o, // 对象
long offset, // 字段偏移量
int expected, // 期望值
int newValue // 新值
);
5.3 CAS 的典型流程
以原子自增为例:
public int incrementAndGet() {
int current;
int next;
do {
current = get(); // 1. 读取当前值 V
next = current + 1; // 2. 计算新值 B
} while (!compareAndSet(current, next)); // 3. CAS(V, current, next)
return next;
}
流程图:
开始
│
▼
读取当前值 current(V)
│
▼
计算 next = current + 1(B)
│
▼
CAS(V, current, next) ──成功──→ 返回 next
│
失败(V 已被其他线程修改)
│
▼
重新读取 current(自旋重试)
│
▼
└──→ 回到计算 next
5.4 CAS 的三大问题
问题一:ABA 问题
CAS 在比较时只关心值是否相等,如果值从 A 变成 B 又变回 A,CAS 会认为值没有被修改,从而产生错误。
// 线程1:读取 value = A
// 线程2:将 value 从 A → B → A
// 线程1:CAS(A, A, C) —— 成功!但中间的修改被忽略了
解决方案:使用版本号/时间戳。Java 提供了 AtomicStampedReference 和 AtomicMarkableReference:
// AtomicStampedReference 使用版本号解决 ABA 问题
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);
int stamp = ref.getStamp(); // 获取当前版本号
int oldValue = ref.getReference(); // 获取当前值
// CAS 时同时比较值和版本号
boolean success = ref.compareAndSet(oldValue, 101, stamp, stamp + 1);
问题二:自旋开销
如果 CAS 长时间失败(高并发竞争激烈),会导致 CPU 不断自旋,浪费 CPU 资源。
解决方案:
- 限制自旋次数
- 使用
LongAdder(分段 CAS)分散竞争 - 在适当时候让出 CPU(
Thread.yield())
问题三:只能保证单个共享变量的原子操作
CAS 只能对单个变量进行原子操作,当需要同时更新多个变量时,CAS 无能为力。
解决方案:
- 将多个变量封装到一个对象中,使用
AtomicReference - 使用
synchronized或Lock
5.5 Unsafe 类简介
sun.misc.Unsafe 是 Java 中一个特殊的类,它提供了直接操作内存和执行底层 CAS 操作的能力:
public class UnsafeExample {
private static final Unsafe UNSAFE;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
UNSAFE = (Unsafe) field.get(null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 使用 Unsafe 进行原子操作
public boolean casInt(Object obj, long offset, int expected, int newValue) {
return UNSAFE.compareAndSwapInt(obj, offset, expected, newValue);
}
}
注意:Unsafe 类的设计初衷是供 JDK 内部使用,直接在业务代码中使用存在安全风险。推荐使用 JDK 提供的原子类。
六、Java 原子类全解析
Java 在 java.util.concurrent.atomic 包中提供了一系列原子类,它们基于 CAS 和 volatile 实现线程安全的变量操作。
6.1 原子类分类总览
java.util.concurrent.atomic
├── 基本类型
│ ├── AtomicInteger
│ ├── AtomicLong
│ └── AtomicBoolean
├── 数组类型
│ ├── AtomicIntegerArray
│ ├── AtomicLongArray
│ └── AtomicReferenceArray<E>
├── 引用类型
│ ├── AtomicReference<V>
│ ├── AtomicStampedReference<V>
│ └── AtomicMarkableReference<V>
├── 字段更新器
│ ├── AtomicIntegerFieldUpdater<T>
│ ├── AtomicLongFieldUpdater<T>
│ └── AtomicReferenceFieldUpdater<T,V>
├── 累加器(JDK 8+)
│ ├── LongAdder
│ ├── LongAccumulator
│ ├── DoubleAdder
│ └── DoubleAccumulator
└── 变量句柄(JDK 9+)
└── VarHandle
6.2 基本类型原子类
AtomicInteger
public class AtomicIntegerDemo {
private AtomicInteger count = new AtomicInteger(0);
// 原子自增并返回新值(类似 ++i 的效果)
public int incrementAndGet() {
return count.incrementAndGet();
}
// 原子获取并自增(类似 i++ 的效果,返回旧值)
public int getAndIncrement() {
return count.getAndIncrement();
}
// 原子加法
public int add(int delta) {
return count.addAndGet(delta);
}
// CAS 操作
public boolean compareAndSet(int expect, int update) {
return count.compareAndSet(expect, update);
}
// 原子更新(函数式接口)
public int update(int newValue) {
return count.updateAndGet(x -> newValue);
}
}
AtomicLong
与 AtomicInteger 类似,但操作 long 类型。值得注意的是,JVM 规范中 long 和 double 的读写可以被拆分为两次 32 位操作(非原子性),这意味着在 32 位 JVM 上 long 的读写在极端情况下也需要同步保护。不过,现代主流 JDK(17/21/25 LTS)均已不再提供 32 位版本,此问题在实际中已基本不存在。
JDK 8+ 建议:对于纯累加场景,优先使用
LongAdder替代AtomicLong,性能更优。
AtomicBoolean
常用于一次性标志位控制:
AtomicBoolean initialized = new AtomicBoolean(false);
public void init() {
if (initialized.compareAndSet(false, true)) {
// 只有一个线程能进入此代码块
doInit();
}
}
6.3 数组类型原子类
支持对数组中某个元素的原子操作:
public class AtomicIntegerArrayDemo {
private AtomicIntegerArray array = new AtomicIntegerArray(10);
public void increment(int index) {
array.getAndIncrement(index); // 原子地对指定索引的元素 +1
}
public int get(int index) {
return array.get(index);
}
}
6.4 引用类型原子类
AtomicReference
对任意对象的原子引用更新:
public class AtomicReferenceDemo {
private AtomicReference<User> currentUser = new AtomicReference<>();
public boolean updateUser(User oldUser, User newUser) {
return currentUser.compareAndSet(oldUser, newUser);
}
// 使用函数式原子更新
public void updateName(String newName) {
currentUser.updateAndGet(user ->
new User(user.getId(), newName, user.getAge())
);
}
}
AtomicStampedReference(解决 ABA 问题)
public class AtomicStampedReferenceDemo {
// 初始值 100,初始版本号 1
private AtomicStampedReference<Integer> ref =
new AtomicStampedReference<>(100, 1);
public void safeUpdate() {
int stamp = ref.getStamp();
Integer oldValue = ref.getReference();
// 同时检查值和版本号
boolean success = ref.compareAndSet(oldValue, 101, stamp, stamp + 1);
System.out.println("Update " + (success ? "succeeded" : "failed"));
}
}
AtomicMarkableReference(标记位版本控制)
与 AtomicStampedReference 类似,但使用布尔标记位代替版本号:
// 适用于只需要知道是否被修改过的场景
AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("initial", false);
boolean[] markHolder = new boolean[1];
String value = ref.get(markHolder);
boolean wasMarked = markHolder[0];
ref.compareAndSet("initial", "updated", false, true);
6.5 字段更新器
当需要对已有类的某个字段进行原子更新时(字段必须是 volatile 的),可以使用字段更新器:
public class User {
volatile int age;
// 使用字段更新器
private static final AtomicIntegerFieldUpdater<User> AGE_UPDATER =
AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
public void birthday() {
AGE_UPDATER.incrementAndGet(this);
}
}
6.6 LongAdder vs AtomicLong(JDK 8+)
在高并发场景下,AtomicLong 由于所有线程竞争同一个变量,会导致大量 CAS 失败和自旋。LongAdder 通过**分段 CAS(Striped CAS)**来解决:
AtomicLong 结构:
┌───────────────────┐
│ base (volatile) │ ← 所有线程竞争这一个值
└───────────────────┘
LongAdder 结构:
┌───────────────────┐
│ base (volatile) │ ← 无竞争时直接累加
└───────────────────┘
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│Cell 0│ │Cell 1│ │Cell 2│ │Cell 3│ ← 有竞争时分散到不同 Cell
└─────┘ └─────┘ └─────┘ └─────┘
sum() = base + Cell[0] + Cell[1] + Cell[2] + Cell[3]
// AtomicLong —— 高并发时性能下降
AtomicLong counter = new AtomicLong(0);
counter.incrementAndGet();
// LongAdder —— 高并发时性能更优
LongAdder adder = new LongAdder();
adder.increment(); // +1
adder.add(10); // +10
long sum = adder.sum(); // 获取总和(非精确快照)
| 特性 | AtomicLong | LongAdder |
|---|---|---|
| CAS 竞争度 | 高(单一热点) | 低(分散到多个 Cell) |
| 精确性 | 强一致 | 最终一致 |
| 适用场景 | 需要精确值 | 高并发累加统计 |
| 内存占用 | 低 | 较高(多个 Cell) |
6.7 VarHandle(JDK 9+)
VarHandle 是 JDK 9 引入的变量句柄,旨在替代 Unsafe 类,提供更安全、更标准的底层原子操作 API:
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
public class VarHandleDemo {
private volatile int count;
private static final VarHandle COUNT_HANDLE;
static {
try {
COUNT_HANDLE = MethodHandles.lookup()
.findVarHandle(VarHandleDemo.class, "count", int.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
public void increment() {
// CAS 操作
int oldValue;
do {
oldValue = (int) COUNT_HANDLE.getVolatile(this);
} while (!COUNT_HANDLE.compareAndSet(this, oldValue, oldValue + 1));
}
}
七、AQS 队列同步器
7.1 AQS 简介
AQS(AbstractQueuedSynchronizer) 是 java.util.concurrent.locks 包下的核心抽象类,它是 Java 并发包中大多数同步器的基石。
核心思想:
- 使用一个
volatile int state字段表示同步状态 - 内部维护一个 CLH(Craig, Landin, and Hagersten)变种双端队列,用于存放等待获取同步状态的线程
7.2 AQS 的设计模式
AQS 采用模板方法设计模式:
AbstractQueuedSynchronizer (抽象类)
├── 独占模式(Exclusive)
│ ├── tryAcquire(int) —— 子类实现:尝试获取锁
│ ├── tryRelease(int) —— 子类实现:尝试释放锁
│ └── isHeldExclusively() —— 子类实现:是否被当前线程持有
├── 共享模式(Shared)
│ ├── tryAcquireShared(int) —— 子类实现:尝试获取共享锁
│ └── tryReleaseShared(int) —— 子类实现:尝试释放共享锁
└── 模板方法(AQS 已实现)
├── acquire(int) —— 获取锁(独占)
├── release(int) —— 释放锁(独占)
├── acquireShared(int) —— 获取锁(共享)
├── releaseShared(int) —— 释放锁(共享)
└── ...
7.3 基于 AQS 实现的同步工具
| 同步工具 | 模式 | 说明 |
|---|---|---|
| ReentrantLock | 独占 | 可重入的互斥锁 |
| ReentrantReadWriteLock | 独占 + 共享 | 读写锁 |
| Semaphore | 共享 | 信号量(控制并发访问数量) |
| CountDownLatch | 共享 | 倒计时门闩(让一个或多个线程等待其他线程完成) |
注意:
CyclicBarrier(循环栅栏)虽然也是常用的同步工具,但它并非直接基于 AQS 实现,而是基于ReentrantLock和Condition构建的上层工具。
7.4 ReentrantLock 示例
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // 必须在 finally 中释放锁
}
}
// 支持公平锁
private final ReentrantLock fairLock = new ReentrantLock(true);
// 支持条件变量
private final ReentrantLock lockWithCondition = new ReentrantLock();
private final Condition notEmpty = lockWithCondition.newCondition();
private final Condition notFull = lockWithCondition.newCondition();
}
7.5 CountDownLatch 示例
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
// 模拟工作
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown(); // 计数器 -1
}
}).start();
}
latch.await(); // 主线程等待,直到计数器为 0
System.out.println("所有任务完成!");
}
}
7.6 Semaphore 示例
public class SemaphoreDemo {
// 允许 3 个线程同时访问
private final Semaphore semaphore = new Semaphore(3);
public void accessResource() {
try {
semaphore.acquire(); // 获取许可
System.out.println(Thread.currentThread().getName() + " 正在访问资源");
Thread.sleep(2000); // 模拟使用资源
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release(); // 释放许可
}
}
}
7.7 手写一个简单的互斥锁
通过继承 AQS 来理解其工作原理:
public class SimpleMutexLock {
private final Sync sync = new Sync();
// AQS 的内部同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 尝试获取锁(独占模式)
@Override
protected boolean tryAcquire(int arg) {
// 使用 CAS 将 state 从 0 改为 1
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 尝试释放锁
@Override
protected boolean tryRelease(int arg) {
setExclusiveOwnerThread(null);
setState(0); // volatile 写,保证可见性
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1 && getExclusiveOwnerThread() == Thread.currentThread();
}
}
public void lock() {
sync.acquire(1); // 模板方法:获取锁
}
public void unlock() {
sync.release(1); // 模板方法:释放锁
}
}
八、JVM 内存区域与并发安全
8.1 JVM 运行时数据区
JVM 的运行时数据区划分为以下几部分,理解它们有助于判断线程安全性:
┌─────────────────────────────────────────────────┐
│ JVM 运行时数据区 │
├──────────────┬──────────────────────────────────┤
│ 线程私有区域 │ │
│ ┌──────────┐ │ │
│ │程序计数器 │ │ 所有线程共享 │
│ └──────────┘ │ ┌────────────┐ ┌────────────┐ │
│ ┌──────────┐ │ │ Java 堆 │ │ 方法区 │ │
│ │ JVM 栈 │ │ │ (对象、数组) │ │(类信息、常量)│ │
│ │ (栈帧) │ │ │ │ │ │ │
│ │ - 局部变量│ │ │ ┌────────┐ │ │ ┌────────┐ │ │
│ │ - 操作数栈│ │ │ │ 新生代 │ │ │ │运行时常量池│ │ │
│ │ - 动态链接│ │ │ │ 老年代 │ │ │ └────────┘ │ │
│ └──────────┘ │ │ └────────┘ │ └────────────┘ │
│ ┌──────────┐ │ └────────────┘ │
│ │本地方法栈 │ │ │
│ └──────────┘ │ │
└──────────────┴──────────────────────────────────┘
8.2 各区域与线程安全的关系
| 内存区域 | 线程共享? | 线程安全? | 说明 |
|---|---|---|---|
| 程序计数器 | 线程私有 | 天然安全 | 每个线程独立拥有,存储当前执行的字节码行号 |
| JVM 栈 | 线程私有 | 天然安全 | 存储局部变量和方法调用信息,局部变量不存在共享问题 |
| 本地方法栈 | 线程私有 | 天然安全 | 为 Native 方法服务 |
| Java 堆 | 线程共享 | 不安全 | 存放对象实例和数组,是 GC 的主要区域,也是线程安全问题的高发区 |
| 方法区 | 线程共享 | 不安全 | 存储类信息、常量、静态变量,多个线程可能同时访问 |
| 运行时常量池 | 线程共享 | 需注意 | 属于方法区的一部分,String.intern() 等操作需要注意并发 |
8.3 垃圾分代回收机制
Java 堆采用分代回收策略:
┌──────────────────────────────┐
│ Java 堆 │
├──────────┬───────────────────┤
│ 新生代 │ 老年代 │
│ (Young) │ (Old/Tenured) │
│ ┌──────┐ │ │
│ │Eden │ │ │
│ │ │ │ │
│ ├──────┤ │ │
│ │Survivor││ │
│ │ S0 │ │ │
│ │ S1 │ │ │
│ └──────┘ │ │
└──────────┴───────────────────┘
GC 与并发的关系:
- STW(Stop-The-World):GC 时会暂停所有应用线程,影响系统响应时间
- 安全点(Safepoint):GC 暂停线程的位置,线程需要到达安全点才能被暂停
- 并发标记:G1、ZGC、Shenandoah 等现代垃圾收集器在标记阶段与应用线程并发执行,大幅减少了 STW 时间
注意:CMS(Concurrent Mark Sweep)收集器已在 JDK 9 被标记为废弃(@Deprecated),在 JDK 14 中被正式移除。当前主流的 GC 选择为 G1(JDK 9+ 默认)、ZGC(JDK 15+ 生产可用,JDK 21+ 默认分代 ZGC)和 Shenandoah。
注意:虽然 GC 会暂停应用线程,但这与线程安全问题无关。GC 暂停所有线程后,对象引用的一致性由 GC 算法保证,不会导致并发修改问题。
8.4 逃逸分析与线程安全
JVM 的逃逸分析(Escape Analysis) 可以判断对象是否只在一个线程内使用:
public class EscapeAnalysisExample {
// 未逃逸——对象不会暴露到方法外,JIT 可能将其分配在栈上
public void notEscape() {
Date date = new Date(); // 栈上分配(JIT 优化后)
System.out.println(date);
}
// 方法逃逸——对象通过返回值暴露到外部
public Date methodEscape() {
return new Date(); // 堆上分配
}
// 线程逃逸——对象被传递到其他线程
public void threadEscape() {
new Thread(() -> {
Date date = new Date(); // 堆上分配
}).start();
}
}
如果对象没有逃逸,JIT 编译器可以进行以下优化:
- 标量替换(Scalar Replacement):将对象拆散,用基本类型变量代替
- 锁消除(Lock Elimination):消除不必要的同步操作
- 栈上分配(Stack Allocation):将对象分配在栈上,随方法结束自动释放
九、实战对比与最佳实践
9.1 各种原子性方案对比
| 方案 | 性能 | 编码复杂度 | 灵活性 | 死锁风险 | 适用场景 |
|---|---|---|---|---|---|
| volatile | 最高 | 低 | 低(单变量) | 无 | 状态标志、DCL |
| synchronized | 中(优化后不错) | 低 | 中 | 有 | 代码块/方法同步 |
| ReentrantLock | 中 | 中 | 高 | 有 | 需要公平锁/条件变量 |
| Atomic 原子类 | 高(低竞争) | 低 | 低(单变量) | 无 | 计数器、序列号 |
| LongAdder | 最高(高并发) | 低 | 低 | 无 | 高并发统计 |
| CAS(Unsafe/VarHandle) | 高 | 高 | 高 | 无 | 框架级开发 |
9.2 何时使用哪种方案?
需要保证原子性?
│
├── 单个变量的简单读写?
│ ├── 是 → volatile 即可
│ └── 否 → 继续判断
│
├── 单个变量的读-改-写操作?
│ ├── 高并发累加 → LongAdder / LongAccumulator
│ ├── 需要精确值 → AtomicInteger / AtomicLong
│ └── 对象引用 → AtomicReference
│
├── 多个变量的复合操作?
│ ├── 需要条件等待 → ReentrantLock + Condition
│ ├── 不需要条件等待 → synchronized 代码块
│ └── 已有对象引用 → AtomicReference(包装为对象)
│
└── 需要构建自定义同步工具?
└── 继承 AQS
9.3 常见陷阱与最佳实践
陷阱一:在循环中使用 CAS 不加限制
// 危险:可能长时间自旋
while (!atomicRef.compareAndSet(old, newVal)) {
// 无限自旋
}
// 推荐:加入重试次数限制
int retries = 0;
while (!atomicRef.compareAndSet(old, newVal) && retries < MAX_RETRIES) {
retries++;
Thread.yield(); // 让出 CPU
}
陷阱二:AtomicReference 的 ABA 问题
// 危险:忽略 ABA 问题
AtomicReference<Node> head = new AtomicReference<>(nodeA);
// 其他线程将 nodeA → nodeB → nodeA
// 推荐:使用 AtomicStampedReference
AtomicStampedReference<Node> stampedHead =
new AtomicStampedReference<>(nodeA, 0);
陷阱三:LongAdder 的 sum() 不精确
LongAdder adder = new LongAdder();
// 并发环境中调用 sum() 时,可能有线程正在执行 add()
// sum() 返回的是快照值,不是精确的一致快照
long snapshot = adder.sum(); // 近似值,非精确值
最佳实践总结:
- 优先使用高层 API:能用
LongAdder就不要用Unsafe - 尽量缩小同步范围:synchronized 代码块越小越好
- 始终在 finally 中释放锁:防止异常导致锁无法释放
- 避免嵌套锁:降低死锁风险
- 使用并发容器:
ConcurrentHashMap、CopyOnWriteArrayList等已经内置了线程安全机制 - 优先考虑不可变对象:不可变对象天生线程安全
十、总结
本文从原子性的基本概念出发,系统地介绍了 Java 并发编程中保证原子性的各种机制:
| 主题 | 核心要点 |
|---|---|
| 原子性本质 | 操作不可分割,要么全做要么不做 |
| JMM | 通过 happens-before 规则定义线程间操作的可见性和有序性 |
| volatile | 保证可见性和有序性,但不保证原子性 |
| synchronized | 互斥锁,保证代码块的原子性,JDK 1.6+ 有锁优化 |
| CAS | 无锁算法的基石,存在 ABA、自旋开销、单变量限制三大问题 |
| 原子类 | 基于 CAS + volatile,提供丰富的原子操作 API |
| AQS | 并发包的基石,ReentrantLock、Semaphore 等都基于它实现 |
| JVM 内存区域 | 线程私有区域天然安全,共享区域(堆、方法区)需要同步 |
补充:JDK 21+ 虚拟线程对并发编程的影响
JDK 21 正式引入了虚拟线程(Virtual Threads,Project Loom),这是 Java 并发编程自诞生以来最大的变革之一。虚拟线程对原子性的讨论有重要影响:
虚拟线程的核心理念:
- 虚拟线程是由 JVM 管理的轻量级线程,一个应用可以轻松创建数百万个虚拟线程
- 当虚拟线程遇到阻塞 I/O 操作时,JVM 会自动将其卸载(unmount),将底层载体线程(Carrier Thread)让给其他虚拟线程使用
- 虚拟线程是
Thread的子类,现有的并发 API(synchronized、Atomic、Lock等)无需修改即可使用
虚拟线程与锁的注意事项:
// 在虚拟线程中使用 synchronized 是安全的,但可能导致"载体线程钉住"问题:
// 如果虚拟线程在 synchronized 块内执行 I/O 操作,
// 底层的载体线程无法被释放,影响整体吞吐量
// 推荐:在虚拟线程环境中优先使用 ReentrantLock
private final ReentrantLock lock = new ReentrantLock();
public void processWithVirtualThread() {
lock.lock();
try {
// 即使包含阻塞 I/O,载体线程也能正确释放
blockingIO();
} finally {
lock.unlock();
}
}
虚拟线程环境下的原子性方案推荐:
| 场景 | 平台线程时代 | 虚拟线程时代 |
|---|---|---|
| 互斥同步 | synchronized 即可 |
优先 ReentrantLock |
| 原子变量 | AtomicInteger 等 |
不变 |
| 并发统计 | LongAdder |
不变,但竞争压力因线程数激增可能更大 |
| 等待/通知 | synchronized + wait/notify |
优先 ReentrantLock + Condition |
关键提示:虚拟线程改变的是线程模型和阻塞行为的处理方式,并没有改变原子性的基本规则。共享变量的并发修改依然需要同步保护,CAS 和原子类的原理完全不变。
在实际开发中,选择合适的原子性方案需要根据具体场景权衡性能、编码复杂度和灵活性。理解底层原理,才能在复杂场景下做出正确的技术选型。
参考资源:
更多推荐
所有评论(0)