深入理解 Java 并发编程中的原子性:从原理到实战

原子性是多线程编程中最基本也最容易被忽视的问题之一。本文将从 Java 内存模型出发,深入剖析原子性的本质、CAS 底层实现、原子类的使用,以及 AQS 等高级同步机制,帮助你构建完整的并发编程知识体系。


目录


一、什么是原子性?

原子性(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 的核心目标是解决并发编程中的三个关键问题:

  1. 可见性(Visibility):一个线程修改了共享变量,其他线程能够立刻看到修改后的值
  2. 有序性(Ordering):程序的执行顺序按照代码的先后顺序
  3. 原子性(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 提供的一种轻量级同步机制,它提供了以下保证:

  1. 可见性:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个 volatile 变量,其他线程能立刻看到修改后的值
  2. 有序性(禁止指令重排序):编译器和处理器不会对 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 提供了 AtomicStampedReferenceAtomicMarkableReference

// 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
  • 使用 synchronizedLock

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 规范中 longdouble 的读写可以被拆分为两次 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 实现,而是基于 ReentrantLockCondition 构建的上层工具。

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();  // 近似值,非精确值

最佳实践总结

  1. 优先使用高层 API:能用 LongAdder 就不要用 Unsafe
  2. 尽量缩小同步范围:synchronized 代码块越小越好
  3. 始终在 finally 中释放锁:防止异常导致锁无法释放
  4. 避免嵌套锁:降低死锁风险
  5. 使用并发容器ConcurrentHashMapCopyOnWriteArrayList 等已经内置了线程安全机制
  6. 优先考虑不可变对象:不可变对象天生线程安全

十、总结

本文从原子性的基本概念出发,系统地介绍了 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(synchronizedAtomicLock 等)无需修改即可使用

虚拟线程与锁的注意事项

// 在虚拟线程中使用 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 和原子类的原理完全不变。

在实际开发中,选择合适的原子性方案需要根据具体场景权衡性能、编码复杂度和灵活性。理解底层原理,才能在复杂场景下做出正确的技术选型。


参考资源

更多推荐