Java 并发编程从入门到精通:线程、锁与线程池全解析

📌 本文由 32 篇相关文章智能合并整理而成

JMM 与 volatile 关键字:Java 内存模型的底层真相

一、引言:从 CPU 到 Java——为什么需要内存模型?

写 Java 并发代码时,”多线程下变量不可见”、”指令重排导致奇怪的结果”这类问题困扰着每一个开发者。本质上,这些问题都源于同一个根源:CPU、编译器、内存三者之间有一个巨大的性能差距

CPU 的主频在过去几十年里从几十 MHz 提升到了 5GHz+,而内存的访问延迟却只从 60ns 降到了约 100ns——CPU 比内存快了至少两个数量级。为了填补这个鸿沟,现代 CPU 和编译器引入了一系列优化:缓存、指令流水线、乱序执行、分支预测。这些优化在多线程环境下,却成了问题的根源——不同 CPU 核心看到的内存视图可能不一致。

这就是 Java 内存模型(Java Memory Model, JMM)要解决的问题:定义一套规则,让我们能写出正确的并发程序,同时不牺牲太多性能。

flowchart TD
    subgraph 问题链
        A[CPU 与内存的速度差距] --> B[引入 CPU Cache]
        B --> C[缓存一致性协议\n如 MESI]
        C --> D[Store Buffer / \nInvalidation Queue]
        D --> E[指令重排\nCPU 和编译器]
        E --> F[可见性问题\n有序性问题]
    end

    subgraph 解决方案
        G[Java Memory Model]
        H[JMM 规范]
        I[volatile / synchronized\nfinal / happens-before]
    end

    F --> G
    G --> H
    H --> I

1.1 从 CPU 到 JMM——层级关系

硬件层面:CPU → 多级缓存 → 内存 → 磁盘
JVM 层面:线程 → 工作内存(线程栈)→ 主内存 → 持久层

flowchart LR
    subgraph 线程 A
        A1[Thread A\n工作内存] -->|read/load| A2[本地缓存]
    end
    subgraph 线程 B
        B1[Thread B\n工作内存] -->|read/load| B2[本地缓存]
    end
    subgraph 主内存
        M1[Heap / Method Area\n共享变量]
    end

    A2 <-->|store/write| M1
    B2 <-->|store/write| M1

    style A2 fill:#3498db,color:#fff
    style B2 fill:#e74c3c,color:#fff
    style M1 fill:#27ae60,color:#fff

JMM 定义了 8 种内存操作及其执行规则:

操作 作用方 目标 说明
lock 线程 主内存 锁定变量(仅一个线程能锁)
unlock 线程 主内存 解锁变量
read 线程 主内存 将主内存的变量读到工作内存
load 线程 工作内存 将 read 得到的值放入本地变量副本
use 线程 工作内存 把工作内存变量值传递给执行引擎
assign 线程 工作内存 将执行引擎的结果赋值给工作内存变量
store 线程 工作内存 将工作内存的变量传递到主内存
write 线程 主内存 将 store 的值写入主内存变量

二、指令重排——看不见的”乱序”

2.1 重排的来源

指令重排有三个层面:

flowchart LR
    subgraph 编译器重排
        COD[源代码] -->|词法/语法分析| AST
        AST -->|JIT 编译器优化| IR[中间代码]
        IR -->|指令调度| ASM[汇编代码]
    end

    subgraph CPU 重排
        ASM -->|取指| IF[Instruction Fetch]
        IF -->|乱序执行| OOE[Out-of-Order Execution]
        OOE -->|提交| RET[Retire / Commit]
    end

    subgraph 内存系统重排
        RET -->|Store Buffer| SB
        SB -->|MESI 协议| CACHE[CPU Cache]
        CACHE -->|写入| MEM[Main Memory]
    end

编译器和 CPU 为什么要重排? 考虑以下代码:

// 原始代码
int a = 1;        // A
int b = 2;        // B
int c = a + b;    // C

从数据依赖来看,C 依赖 A 和 B,但 A 和 B 之间没有依赖关系。CPU 可以:

乱序执行: A → B → C
      或: B → A → C

两者的结果完全一致,但 B 先执行可能让 CPU 的数据缓存命中率更高(如果 b 已经在缓存中的话)。

2.2 重排如何导致并发问题

// 经典例子:双检锁(DCL)
public class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {                    // 检查 1
            synchronized (Singleton.class) {
                if (instance == null) {            // 检查 2
                    instance = new Singleton();    // 问题就在这里!
                }
            }
        }
        return instance;
    }
}

instance = new Singleton() 在字节码层面分为三步:

// 1. memory = allocate();      // 分配内存空间
// 2. ctorInit(memory);         // 调用构造函数初始化
// 3. instance = memory;        // 将引用指向内存

// 如果 2 和 3 被重排为:
// 1. memory = allocate();
// 2. instance = memory;        // 先赋值(此时对象未初始化!)
// 3. ctorInit(memory);         // 后初始化

// 另一个线程在 2 和 3 之间执行检查 1:
// if (instance == null) → false(instance 非 null)
// return instance; → 拿到未初始化的对象!

解决方案: 使用 volatile 禁止重排:

private static volatile Singleton instance;  // volatile 禁止指令重排

2.3 什么是”as-if-serial”语义?

JMM 允许编译器和 CPU 在不改变单线程执行结果的前提下任意重排指令。这个原则叫做 as-if-serial:

// as-if-serial 允许的重排
int a = 1;     // A
int b = 2;     // B ← A 和 B 没有数据依赖,可以重排

// as-if-serial 不允许的重排
int a = 1;     // A
int b = a + 1; // B ← B 依赖 A,禁止重排
int c = b + 1; // C ← C 依赖 B,禁止重排
// 最终执行顺序只能是 A → B → C

这个原则本身是合理的,但在多线程环境下,单个线程的 as-if-serial 无法保证全局的一致性。

三、volatile——最轻量的同步机制

3.1 volatile 的两层语义

public class VolatileDemo {

    // volatile 保证:
    // 1. 可见性:对 volatile 变量的写,会立即被其他线程看到
    // 2. 有序性:禁止 JIT 编译器和 CPU 对 volatile 相关的指令重排
    private volatile boolean flag = false;
    private int data = 0;

    // 线程 A 执行
    public void writer() {
        data = 42;          // 普通写
        flag = true;        // volatile 写(禁止与前面的普通写重排)
    }

    // 线程 B 执行
    public void reader() {
        if (flag) {         // volatile 读
            System.out.println(data);  // 保证看到 42
        }
    }
}

volatile 的可见性如何保证? 在 x86 架构下:

// 对 volatile 变量的写操作,编译器会在生成的汇编中插入一条
// lock 前缀指令(或者内存屏障指令)
//
// lock addl $0, (%rsp)
//
// 这条指令的作用:
// 1. 将当前 CPU 的 Store Buffer 全部写入缓存
// 2. 使其他 CPU 中对应的缓存行失效
// 3. 相当于一个"全屏障"(StoreLoad Barrier)

3.2 volatile 的内存屏障实现

JVM 在 volatile 操作周围插入内存屏障(Memory Barrier)

flowchart LR
    subgraph volatile 
        W1[普通写] --> WB[StoreStore Barrier ]
        WB --> W2[volatile ]
        W2 --> WB2[StoreLoad Barrier ]
    end

    subgraph volatile 
        VB[LoadLoad Barrier ] --> V1[volatile ]
        V1 --> VB2[LoadStore Barrier ]
        VB2 --> V2[普通读]
    end

JMM 中的四种内存屏障:

屏障类型 指令组合 作用
LoadLoad Load1 → LoadLoad → Load2 禁止 Load1 和后续 Load2 的重排
StoreStore Store1 → StoreStore → Store2 禁止 Store1 和后续 Store2 的重排
LoadStore Load1 → LoadStore → Store2 禁止 Load1 和后续 Store2 的重排
StoreLoad Store1 → StoreLoad → Load2 全屏障,所有屏障中最重的

注意: StoreLoad 屏障的开销最大,因为它需要清空 Store Buffer 并等待写完成。volatile 写操作在 x86 上插入的就是 StoreLoad 屏障。

3.3 volatile 不能保证原子性

// ❌ 反例:volatile 不能保证复合操作的原子性
public class VolatileCounter {
    private volatile int count = 0;

    public void increment() {
        count++;  // 这行代码不是原子的!
    }
    // count++ 在字节码层面:
    // 1. GETFIELD count      // read
    // 2. ICONST_1            // 常量 1
    // 3. IADD                // add
    // 4. PUTFIELD count      // write
    // 虽然每一步都是原子的,但整体不是!
    // 可能出现线程 A 读到 5,线程 B 也读到 5
    // A 写回 6,B 也写回 6 → 丢失一次递增
}

// ✅ 正例:使用 AtomicInteger
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
    count.incrementAndGet();  // CAS 保证原子性
}

// ✅ 或者使用 synchronized
private int count = 0;
public synchronized void increment() {
    count++;
}

3.4 volatile 的适用场景

// ✅ 适用场景 1:状态标志
public class ShutdownDemo {
    private volatile boolean shutdown;

    public void shutdown() {
        shutdown = true;  // 工作线程通过读 volatile 值感知关闭
    }

    public void doWork() {
        while (!shutdown) {
            // 处理任务
        }
    }
}

// ✅ 适用场景 2:一次性安全发布(one-time safe publication)
public class SafePublication {
    private volatile Map<String, String> config;

    public void updateConfig(Map<String, String> newConfig) {
        // 对 newConfig 的所有写操作
        // 在 volatile 写之前已经完成
        this.config = new ConcurrentHashMap<>(newConfig);
        // volatile 写确保上述构造函数的结果对其他线程可见
    }

    public String getConfig(String key) {
        Map<String, String> c = config;  // volatile 读
        return c != null ? c.get(key) : null;
    }
}

// ✅ 适用场景 3:观察者模式中的事件通知
private volatile boolean eventFired;

volatile 不适合的场景:
– 需要原子性的复合操作(count++)
– 多个变量间的逻辑约束(如 x==0 时 y==1 这样的不变量)
– 需要显式锁的复杂条件

四、Happens-Before——JMM 的灵魂

4.1 什么是 Happens-Before?

Happens-Before 是 JMM 中最核心的概念。它不是”时间上的先后”,而是一种可见性保证——如果 A happens-before B,那么 A 操作的结果对 B 操作是可见的,且 A 的排序(如果被重排)一定在 B 之前完成。

4.2 JMM 中的 8 条 Happens-Before 规则

flowchart TD
    subgraph Happens-Before 规则
        R1[程序次序规则\n同一线程中, 写在前面的 happens-before 后面的]
        R2[volatile 变量规则\n对一个 volatile 的写, happens-before\n任意后续对这个 volatile 的读]
        R3[锁规则\n对一个锁的 unlock, happens-before\n后续对这个锁的 lock]
        R4[传递性\nA happens-before B, B happens-before C\n A happens-before C]
        R5[线程启动规则\nThread.start() happens-before\n被启动线程的所有操作]
        R6[线程终止规则\n被终止线程的所有操作 happens-before\nThread.join() 返回]
        R7[线程中断规则\nThread.interrupt() happens-before\n被中断线程检测到中断]
        R8[对象终结规则\n对象构造完成 happens-before\nfinalize() 开始]
    end

应用传递性链来分析经典问题:

public class HappensBeforeChain {
    private int x = 0;
    private volatile boolean flag = false;

    public void writer() {
        x = 42;                       // 操作 A(普通写)
        flag = true;                  // 操作 B(volatile 写)
    }

    public void reader() {
        if (flag) {                   // 操作 C(volatile 读)
            int r = x;                // 操作 D(普通读)
            // 根据规则:
            // A happens-before B(程序次序规则)
            // B happens-before C(volatile 规则)
            // C happens-before D(程序次序规则)
            // → A happens-before D(传递性)
            // → r == 42(保证)
            assert r == 42;
        }
    }
}

4.3 锁规则详解

public class LockRule {
    private int x = 0;
    private final Object lock = new Object();

    // 线程 A
    public void lockWriter() {
        synchronized (lock) {     // lock → unlock
            x = 42;
        }                         // unlock
    }

    // 线程 B
    public void lockReader() {
        synchronized (lock) {     // lock(同一个锁)
            int r = x;            // 保证看到 x == 42
        }
    }
}

锁规则的关键:必须是对同一个监视器对象的 unlock → lock。如果线程 A 对 lock1 解锁,线程 B 对 lock2 加锁,则没有 happens-before 关系。

五、final 的内存语义

5.1 final 的多线程保证

// final 字段在构造函数返回之前被正确构造的情况下,
// 其他线程保证能看到 final 字段的正确值
public class FinalExample {
    private final int x;   // final 字段
    private int y;         // 普通字段
    private static FinalExample instance;

    public FinalExample() {
        x = 42;            // final 写
        y = 10;            // 普通写
    }

    public static void writer() {
        instance = new FinalExample();
    }

    public static void reader() {
        FinalExample obj = instance;
        if (obj != null) {
            int r1 = obj.x;  // 保证是 42(final 保证)
            int r2 = obj.y;  // 可能是 0(普通字段无保证)
        }
    }
}

final 的内存语义在 JSR 133 中得到加强:
– 构造函数中对 final 字段的写,与构造函数返回后的引用赋值之间,禁止重排
– 构造函数中修改 final 字段的写,禁止与后续可能把对象引用发布到其他线程的操作一起重排

六、JMM 在实际编码中的应用

6.1 安全发布对象的四种方式

public class SafePublish {
    private Map<String, String> config;

    // 方式 1:通过 volatile 发布
    private volatile List<String> list1;
    public void setList1(List<String> list) {
        this.list1 = Collections.unmodifiableList(new ArrayList<>(list));
    }

    // 方式 2:通过 synchronized 发布
    private List<String> list2;
    public synchronized void setList2(List<String> list) {
        this.list2 = Collections.unmodifiableList(new ArrayList<>(list));
    }
    public synchronized List<String> getList2() {
        return list2;
    }

    // 方式 3:通过 final 发布
    private static class ConfigHolder {
        static final Map<String, String> CONFIG = loadConfig();
    }
    public static Map<String, String> getConfig() {
        return ConfigHolder.CONFIG;
    }

    // 方式 4:通过原子引用发布
    private final AtomicReference<List<String>> listRef = new AtomicReference<>();
    public void setList(List<String> list) {
        listRef.set(Collections.unmodifiableList(new ArrayList<>(list)));
    }
    public List<String> getList() {
        return listRef.get();
    }
}

6.2 JMM 在不同 CPU 架构上的差异

架构 特点 对 JMM 的影响
x86-TSO 相对保守,只有 StoreLoad 是真正的屏障 其他屏障都是 CPU 级 NOP
ARM / PowerPC 极弱内存模型,几乎任意重排 JMM 需要更多屏障指令
RISC-V 可配置,支持 TSO 和 WMO 与 JMM 的兼容性取决于具体实现

反面教材:x86 上跑得好的代码在 ARM 上出问题

// 在 x86 上,普通写不会和普通读重排(x86 保证)
// 但在 ARM 上,下面的代码可能有问题:
public class WeakMemoryIssue {
    private int a, b;
    private int x, y;

    // 线程 A
    public void writeA() { a = 1; x = b; }
    // 线程 B
    public void writeB() { b = 1; y = a; }

    // 在 x86 上,不可能出现 (x==0 && y==0)
    // 在 ARM/PowerPC 上,可能出现!
}

七、JMM 的底层实现

7.1 C++ 内存模型与 JMM 的关系

JVM 是用 C++ 实现的,JMM 最终通过底层的内存屏障原语来实现。HotSpot 在不同的 CPU 架构上使用不同的屏障实现:

// HotSpot 源码(os_cpu 目录下)中的内存屏障实现
// x86 版本:
inline void OrderAccess::loadload()   { compiler_barrier(); }
inline void OrderAccess::storestore() { compiler_barrier(); }
inline void OrderAccess::loadstore()  { compiler_barrier(); }
inline void OrderAccess::storeload()  {
    // 唯一的硬件屏障
    __asm__ volatile ("mfence" : : : "memory");
}

// ARM 版本:
inline void OrderAccess::loadload()   { __asm__ volatile ("dmb ishld" : : : "memory"); }
inline void OrderAccess::storestore() { __asm__ volatile ("dmb ishst" : : : "memory"); }
inline void OrderAccess::loadstore()  { __asm__ volatile ("dmb ish" : : : "memory"); }
inline void OrderAccess::storeload()  { __asm__ volatile ("dmb ish" : : : "memory"); }

为什么 x86 上的 LoadLoad、StoreStore、LoadStore 屏障都是空的? 因为 x86-TSO 内存模型本身就保证了不会对这些操作进行重排。只有在跨写入和读取时(StoreLoad),才需要真正的硬件屏障。

7.2 编译器屏障 vs 硬件屏障

// 编译器屏障:阻止编译器重排
inline void compiler_barrier() {
    __asm__ volatile ("" : : : "memory");  // 阻止编译器优化
}

// 硬件屏障:阻止 CPU 重排
inline void hardware_barrier() {
    __asm__ volatile ("mfence" : : : "memory");  // x86 StoreLoad 屏障
}

区别: 编译器屏障只影响编译器的指令排序,不影响 CPU 的乱序执行。在 x86 上,除了 StoreLoad 不需要硬件屏障的原因是 x86 的 TSO 模型天然保证了其他类型的排序约束。

八、JMM 常见误解与最佳实践

8.1 误解澄清

// 误解 1:volatile 比 synchronized 快
// 事实:在低竞争场景下 volatile 更快
//       在高竞争场景下 volatile 的自旋可能会导致吞吐量下降
//       并不总是比 synchronized 快

// 误解 2:JMM 保证 64 位变量的读写是原子的
// 事实:JMM 保证 volatile long/double 是原子的
//       非 volatile long/double 在 32 位 JVM 上可能不是原子的
//       (JDK 9+ 中已经修复,默认原子访问)

// 误解 3:final 字段初始化后永远不会变
// 事实:通过反射可以修改 final 字段
//       反射修改后的值不能保证可见性

// 误解 4:加了 synchronized 就不用关心 JMM
// 事实:synchronized 确实提供了完整的可见性保证
//       但理解 JMM 有助于写出更高效的并发代码
//       比如知道哪些操作可以移出同步块

8.2 编写安全并发代码的检查清单

检查项 说明
读写共享变量是否同步? volatile 或 synchronized 或原子类
复合操作是否原子? i++ → AtomicInteger; if-then-act → CAS
对象是否安全发布? 检查构造函数中是否 this 逸出
多变量之间有逻辑约束? 需要使用锁确保原子性
是否依赖 platform-specific 行为? x86 上能跑不等于 ARM 上能跑

九、总结

  1. JMM 定义了 Java 并发编程的”宪法”。 它规定了线程之间如何通过共享内存通信,以及什么情况下的重排是可见的。”happens-before”规则是理解 JMM 的钥匙。

  2. volatile 是最轻量的线程同步机制。 它提供了可见性和有序性,但不提供原子性。合理使用 volatile(状态标志、一次性安全发布等场景)可以减少对锁的依赖,提升性能。

  3. 内存屏障是 JMM 的硬件映射。 不同 CPU 架构对 JMM 的支持程度不同。x86 的能力最强(只在 StoreLoad 需要硬件屏障),ARM/PowerPC 需要更多的硬件屏障指令。这也是为什么在 x86 上测试通过的并发代码,迁移到 ARM 服务器上可能出现问题。

  4. final 在 JSR 133 后获得了增强的内存语义。 正确构造的 final 字段可以安全地在多线程间共享,这是不可变对象并发安全的基础保证。

  5. Happens-Before 规则链是分析并发问题的第一步。 在讨论任何并发问题之前,先画出 happens-before 关系链,这是比阅读代码本身更重要的步骤。

  6. 工具辅助分析。 Java 提供了 jcstress(并发压力测试)来验证并发代码的正确性,建议在编写复杂并发逻辑时使用。


从同步器到 AQS:Java 并发锁的底层实现逻辑

一、引言:Java 锁的进化史

Java 并发锁的发展史,就是一部从”臃肿缓慢”到”轻量高效”的进化史。JDK 1.0 就提供了 synchronized 关键字,但初期实现极其笨重——它是一个”重量级锁”,直接依赖操作系统的互斥量(mutex),线程阻塞和唤醒都需要内核态切换,在 JDK 1.5 之前一直被诟病为”Java 慢”的罪魁祸首之一。

Java 5 的 java.util.concurrent.locks 包引入了 ReentrantLock 等显式锁,基于 Doug Lea 的 AbstractQueuedSynchronizer(AQS) 框架,为 Java 锁的灵活性和性能打开了新世界。但真正让 Java 锁脱胎换骨的是 Java 6 对 synchronized 的优化——引入偏向锁、轻量级锁、锁粗化、锁消除等机制,把 synchronized 从”穷人锁”变成了”万能锁”。

时至今日,synchronized 和 AQS 之间不再是”谁比谁快”的二元竞争关系,而是各有适用场景的伙伴。本文将从源码层面,完整梳理 Java 并发锁的进化之路,带你理解从 synchronized 到 AQS 的底层原理。

flowchart LR
    subgraph JDK 版本演进
        JDK5[JDK 5] -->|引入| ReentrantLock
        JDK5 -->|引入| AQS
        JDK6[JDK 6] -->|优化| Synchronized
        JDK6 -->|偏向锁| BiasLock
        JDK6 -->|轻量级锁| LightLock
        JDK6 -->|锁消除| LockElimination
        JDK15[JDK 15] -->|默认关闭| BiasLock
        JDK21[JDK 21] -->|虚拟线程| VirtualThread
    end

    Synchronized -->|重量级| OS_Mutex[OS Mutex]
    ReentrantLock --> AQS
    AQS -->|基于| CAS[无锁 CAS]

二、synchronized——从字节码到锁升级

2.1 字节码中的 synchronized

// synchronized 的三种用法
public class SynchronizedDemo {

    // 1. 普通同步方法,锁的是当前实例对象
    public synchronized void instanceMethod() {
        // 方法体
    }

    // 2. 静态同步方法,锁的是当前类的 Class 对象
    public static synchronized void staticMethod() {
        // 方法体
    }

    // 3. 同步代码块,锁的是指定的对象
    public void blockMethod() {
        synchronized (this) {
            // 临界区
        }
    }
}

字节码层面分析:

// 同步方法的字节码特征:
// ACC_SYNCHRONIZED 标志在方法访问标志中
// JVM 根据这个标志判断是否需要获取锁

// 同步代码块的字节码特征:
// monitorenter  — 获取指定对象的监视器锁
// monitorexit   — 释放锁(两个:正常退出 + 异常退出)

// 反编译结果:
// 0: aload_0
// 1: dup
// 2: astore_1
// 3: monitorenter         ← 进入同步块,尝试获取锁
// 4: aload_1
// 5: monitorexit          ← 正常退出,释放锁
// 6: goto 14
// 9: astore_2
// 10: aload_1
// 11: monitorexit         ← 异常退出,保证释放锁
// 12: aload_2
// 13: athrow
// 14: return

关键点: JVM 保证了无论同步代码块是正常退出还是异常退出,都会执行 monitorexit。这也是 synchronized 被称为”内置锁”的原因——你永远不用担心忘记释放锁。

2.2 锁升级——从无锁到重量级

JDK 6 之后,synchronized 引入了锁升级机制,锁的状态从低到高依次为:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。锁只能升级不能降级:

flowchart TD
    N[无锁状态] -->|第一个线程访问| B[偏向锁]
    B -->|另一个线程竞争| L[轻量级锁]
    L -->|自旋失败或\n竞争加剧| H[重量级锁]

    B -->|全局安全点| SB[偏向锁撤销]
    SB --> N

    style B fill:#27ae60,color:#fff
    style L fill:#f39c12,color:#fff
    style H fill:#e74c3c,color:#fff

偏向锁

偏向锁的核心思想是:锁不仅没有竞争,而且一直被同一个线程持有

// Mark Word 结构(32位 JVM)
// 无锁态:    | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 |
// 偏向锁态:   | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 |
// 轻量级锁:   | ptr_to_lock_record:62 | 00 |
// 重量级锁:   | ptr_to_heavyweight_monitor:62 | 10 |

// 偏向锁的获取过程(简化)
// 1. 检查 Mark Word 中偏向锁标志位是否为 1
// 2. 如果是,检查线程 ID 是否指向当前线程
// 3. 如果是,直接进入方法体(CAS 都不需要!)
// 4. 如果不是,通过 CAS 尝试将线程 ID 更新为当前线程
// 5. 如果 CAS 失败,说明有线程竞争,进入偏向锁撤销流程

// 偏向锁撤销流程:
// 1. 等待全局安全点(SafePoint)——所有线程暂停
// 2. 检查持有偏向锁的线程是否还活着
//    - 如果已结束:将对象头恢复为无锁态
//    - 如果还在执行:升级为轻量级锁或重量级锁
// 3. 恢复执行

偏向锁在 JDK 15 被默认关闭,原因是它的实现复杂且维护成本高,在大多数现代应用中收益越来越小。但在某些场景(如只有一个线程访问的 synchronized 方法)中,它的性能优势依然明显。

轻量级锁

当偏向锁被撤销后,锁升级为轻量级锁。轻量级锁假设:虽然有竞争,但线程是交替执行的,没有真正的争抢

// 轻量级锁加锁过程
// 1. 在当前线程栈帧中创建锁记录(Lock Record)空间
// 2. 用 CAS 尝试将对象的 Mark Word 替换为锁记录的指针
// 3. 如果 CAS 成功:成功获取轻量级锁
// 4. 如果 CAS 失败:先自旋重试,自旋失败后膨胀为重量级锁

// 轻量级锁的关键:不涉及操作系统的线程阻塞
// 线程通过自旋(忙等待)来等待锁释放
// 如果等待时间短,性能远高于重量级锁
// 如果等待时间长,自旋浪费 CPU

// JVM 还引入了自适应自旋(Adaptive Spinning)
// 根据上次自旋成功与否自动调整自旋次数
// - 上次自旋成功:这次多自旋几次
// - 上次自旋失败:少自旋或不自旋

重量级锁

当锁竞争真正激烈时,轻量级锁会膨胀为重量级锁:

// 重量级锁的核心:依赖操作系统 mutex
// 1. 申请 os::PlatformEvent::park() 阻塞线程
// 2. 锁释放时调用 os::PlatformEvent::unpark() 唤醒线程
// 3. 每次 park/unpark 都需要内核态切换(约 1~10μs)
// 4. 被阻塞的线程进入 Entry List 等待

// 重量级锁中还包含 cxq(竞争队列)和 EntryList(等待队列)
// 这是 JDK 内部对操作系统的优化封装
// 1. 新竞争的线程先进入 cxq(LIFO)
// 2. 被唤醒的线程从 cxq 迁移到 EntryList
// 3. 锁释放时从 EntryList 头部取一个线程唤醒

锁升级对性能的影响:

锁状态 获取成本 释放成本 适用场景
偏向锁 首次 CAS(之后无需操作) 几乎为 0 单线程访问
轻量级锁 CAS + 可能的少量自旋 CAS 低竞争交替执行
重量级锁 线程挂起 + 内核切换 线程唤醒 + 内核切换 高竞争长时间等待

2.3 synchronized 的优化技巧

// ❌ 反例:锁粒度太大
public class CoarseLock {
    public synchronized void process() {
        step1();  // 不需要同步
        step2();  // 需要同步
        step3();  // 不需要同步
        step4();  // 需要同步
    }
}

// ✅ 正例:减小锁粒度
public class FineGrainedLock {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    private final Object lock3 = new Object();

    public void process() {
        step1();
        synchronized (lock1) { step2(); }
        step3();
        synchronized (lock2) { step4(); }
    }
}

// ✅ 正例:使用细粒度锁 + 分段锁思想
public class StripedLock {
    private static final int SHARD_COUNT = 16;
    private final Object[] locks = new Object[SHARD_COUNT];
    private final Map<String, Object> cache = new HashMap<>();

    public StripedLock() {
        for (int i = 0; i < SHARD_COUNT; i++) {
            locks[i] = new Object();
        }
    }

    public Object getFromCache(String key) {
        int shard = Math.abs(key.hashCode() % SHARD_COUNT);
        synchronized (locks[shard]) {
            return cache.get(key);
        }
    }
}

三、AQS——Java 并发锁的骨架

3.1 AQS 是什么?

AbstractQueuedSynchronizer 是 Java 并发包(JUC)的基石。它是一个抽象的同步器框架,定义了多线程访问共享资源的底层模型。JUC 中大部分同步器都是基于 AQS 实现的:

同步器 基于 AQS 的模式 说明
ReentrantLock 独占模式(排他锁) 支持公平/非公平、可中断、超时
ReentrantReadWriteLock 独占 + 共享(读写锁) 读读共享、读写互斥
Semaphore 共享模式(信号量) 资源计数
CountDownLatch 共享模式(门闩) 等待线程完成
ThreadPoolExecutor.Worker 独占模式 线程包装
// AQS 的核心数据结构
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    // 核心状态:同步状态
    // 0 表示未锁定,≥1 表示已锁定
    // ReentrantLock 用它记录锁重入次数
    // Semaphore 用它记录剩余许可数
    private volatile int state;

    // 双向链表:等待队列(CLH 锁变体)
    // 头节点:当前持有锁的线程
    // 尾节点:最新入队的等待线程
    private transient volatile Node head;
    private transient volatile Node tail;
}

3.2 CLH 队列——AQS 的等待队列

AQS 内部维护了一个变体 CLH 队列(一种自旋锁的 FIFO 队列变体):

flowchart LR
    subgraph CLH 等待队列
        HEAD["head\n(当前持有者)"]
        N1["Node\nprev: head\nstatus: SIGNAL"]
        N2["Node\nprev: N1\nstatus: SIGNAL"]
        N3["Node\nprev: N2\nstatus: SIGNAL"]
        TAIL["tail\n(最新入队)"]
    end

    HEAD --> N1 --> N2 --> N3 --> TAIL

    style HEAD fill:#27ae60,color:#fff
    style TAIL fill:#e74c3c,color:#fff
// Node 节点的核心字段
static final class Node {
    // 等待状态
    static final int CANCELLED =  1;  // 取消
    static final int SIGNAL    = -1;  // 后继节点需要被唤醒
    static final int CONDITION = -2;  // 在条件队列中等待
    static final int PROPAGATE = -3; // 共享模式下传播唤醒

    volatile int waitStatus;     // 节点状态
    volatile Node prev;          // 前驱节点
    volatile Node next;          // 后继节点
    volatile Thread thread;      // 等待线程

    Node nextWaiter;  // 条件队列的后继或 Shared 标记
}

关键设计思路: CLH 队列使用的是隐式的前驱通知机制——每个节点在阻塞前会将其前驱节点的 waitStatus 设置为 SIGNAL,这样当前驱节点释放锁时,会检查其后继节点是否需要唤醒。这种方式避免了全局广播,只通知”下一个应该唤醒的线程”。

3.3 AQS 的两种模式

独占模式——以 ReentrantLock 为例

// ReentrantLock.lock() → sync.lock() → AQS.acquire(1)
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&                    // 尝试获取锁
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  // 入队等待
        selfInterrupt();                       // 被中断过则恢复中断标志
}

// tryAcquire 由子类实现——这里展示非公平锁的版本
// java.util.concurrent.locks.ReentrantLock.Sync.nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 锁空闲 → 直接尝试获取(非公平的体现)
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        // 重入:当前线程已持有锁
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;  // 获取失败,进入等待队列
}

// acquireQueued:在队列中等待
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 检查是否需要 park(阻塞)
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

释放锁的过程:

public final boolean release(int arg) {
    if (tryRelease(arg)) {  // 子类实现:减少 state
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);  // 唤醒后继节点
        return true;
    }
    return false;
}

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);  // 清除 SIGNAL

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {  // 后继被取消 → 从尾部向前找有效节点
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);  // 唤醒
}

共享模式——以 Semaphore / CountDownLatch 为例

// 共享模式的 acquire 流程
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)  // 子类实现:返回剩余资源数
        doAcquireShared(arg);
}

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);  // 关键区别:传播!
                    p.next = null;
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

// 共享模式释放时的传播
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head;
    setHead(node);

    // propagate > 0 表示还有剩余资源 → 继续唤醒后继
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();  // 传播唤醒后面的共享节点
    }
}

独占 vs 共享的核心差异:

特性 独占模式 共享模式
state 语义 0=空闲, ≥1=重入次数 剩余资源量
唤醒行为 只唤醒下一个节点 传播唤醒所有共享节点
典型实现 ReentrantLock Semaphore, CountDownLatch
条件变量 支持(Condition) 不支持

3.4 公平锁 vs 非公平锁

// 非公平锁的 tryAcquire(前面已展示)
// 非公平:新来的线程直接 CAS 抢锁,不管队列中有没有等待者
// 优点:吞吐量高(减少线程 park/unpark 开销)
// 缺点:可能导致等待线程"饿死"

// 公平锁的 tryAcquire
static final class FairSync extends Sync {
    @ReservedStackAccess
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 关键区别:先检查队列中是否有等待者
            if (!hasQueuedPredecessors() &&  // 没有等待者才尝试
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

// hasQueuedPredecessors:检查队列中是否有线程等待时间更长
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

性能对比(JMH 基准测试):

竞争程度 公平锁 非公平锁 差异
低竞争(10线程) 85ms 82ms ~3%
中竞争(100线程) 310ms 215ms ~30%
高竞争(1000线程) 2800ms 980ms ~65%

非公平锁在高竞争场景下的优势更明显,因为避免了为”排队”付出线程切换代价。官方推荐默认使用非公平锁。

3.5 Condition——条件变量的实现

Condition 是 AQS 中的另一个重要组件,提供了类似 Object.wait/notify 的线程协作机制:

public class ConditionObject implements Condition, java.io.Serializable {
    // 条件队列(单向链表)
    private transient Node firstWaiter;
    private transient Node lastWaiter;

    // await() → 释放锁 + 进入条件队列等待
    public final void await() throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();

        Node node = addConditionWaiter();     // 加入条件队列
        int savedState = fullyRelease(node);  // 释放当前线程持有的锁
        int interruptMode = 0;

        // 循环等待:直到被转移到同步队列
        while (!isOnSyncQueue(node)) {
            LockSupport.park(this);           // 阻塞
            if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                break;
        }

        // 重新获取锁
        if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
            interruptMode = REINTERRUPT;
        if (node.nextWaiter != null)
            unlinkCancelledWaiters();
        if (interruptMode != 0)
            reportInterruptAfterWait(interruptMode);
    }

    // signal() → 将条件队列头节点转移到同步队列
    public final void signal() {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        Node first = firstWaiter;
        if (first != null)
            doSignal(first);
    }

    // 转移节点到同步队列
    private void doSignal(Node first) {
        do {
            if ((firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            first.nextWaiter = null;
        } while (!transferForSignal(first) &&   // 转移到同步队列
                 (first = firstWaiter) != null);
    }
}
flowchart LR
    subgraph 条件队列
        CW1[Condition Waiter-1]
        CW2[Condition Waiter-2]
        CW3[Condition Waiter-3]
        CW1 --> CW2 --> CW3
    end

    subgraph 同步队列(CLH
        SW1[Node head\n持有锁]
        SW2[Node-2]
        SW3[Node-3]
        SW1 --> SW2 --> SW3
    end

    signal -->|转移第一个条件等待节点| CW1
    CW1 -->|transferForSignal| SW2

    style CW1 fill:#25a25a,color:#fff
    style CW2 fill:#f39c12
    style CW3 fill:#f39c12
    style SW1 fill:#27ae60,color:#fff

四、synchronized vs AQS——如何选择?

// synchronized 适用场景:
// 1. 简单的方法级同步
// 2. 无需超时、可中断等高级特性
// 3. 代码自动管理锁获取和释放(不易出错)
public synchronized void simpleOp() {
    // 一行代码能解决的问题
    this.count++;
}

// AQS 适用场景:
// 1. 需要超时获取锁
// 2. 需要可中断的锁
// 3. 需要公平性
// 4. 需要多个条件变量(Condition)
// 5. 需要读写锁分离
// 6. 需要实现自定义同步器
public void advancedOp() {
    ReentrantLock lock = new ReentrantLock();
    Condition notFull = lock.newCondition();
    Condition notEmpty = lock.newCondition();

    try {
        if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
            try {
                while (queue.isFull()) {
                    notFull.await();  // 等待条件
                }
                queue.put(item);
                notEmpty.signal();
            } finally {
                lock.unlock();
            }
        } else {
            // 超时处理
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

关键对比表:

特性 synchronized ReentrantLock (AQS)
语法简洁性 ✅ 自动管理 ❌ 需要手动加解锁
锁升级 ✅ 偏向→轻量→重量 N/A
可中断
超时获取
公平性 ❌ 非公平 ✅ 可配置
多条件变量 ❌ 单条件(notify) ✅ 多 Condition
读写分离 ✅ ReadWriteLock
是否支持虚拟线程 ✅ 推荐 ⚠️ 尽量使用 synchronized
异常安全 ✅ 自动释放 ⚠️ 必须 finally 释放

五、LockSupport——底层线程阻塞原语

无论是 AQS 还是 synchronized,底层都依赖于 LockSupport 来进行线程的阻塞和唤醒:

public class LockSupport {
    // 本质是调用 Unsafe.park() / unpark()
    // park 会检查当前线程的"许可"(permit)
    // - 如果有许可:立即返回并消费许可
    // - 如果没有许可:阻塞等待

    // 线程阻塞
    public static void park() {
        UNSAFE.park(false, 0L);
    }

    public static void parkNanos(long nanos) {
        if (nanos > 0)
            UNSAFE.park(false, nanos);
    }

    // 线程唤醒(提前给许可)
    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }
}

与 Object.wait/notify 不同,LockSupport 的 park/unpark 是”许可证机制”,不需要在同步块中调用。这意味着 unpark 可以在 park 之前被调用——如果提前调用了 unpark,后续的 park 会直接返回(因为许可已被消费)。

六、实战最佳实践

// 1. 优先使用 synchronized(除非需要高级功能)
// 理由:synchronized 在 JDK 中持续优化,且与虚拟线程兼容性更好

// 2. 使用读写锁分离读多写少的场景
public class ReadWriteCache {
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock r = rwl.readLock();
    private final Lock w = rwl.writeLock();
    private Map<String, Object> cache = new HashMap<>();

    public Object get(String key) {
        r.lock();
        try {
            return cache.get(key);
        } finally {
            r.unlock();
        }
    }

    public void put(String key, Object value) {
        w.lock();
        try {
            cache.put(key, value);
        } finally {
            w.unlock();
        }
    }
}

// 3. 锁的粒度尽量小——能锁代码块绝不锁方法
// 4. 尽量避免锁嵌套(死锁风险)
// 5. 高竞争场景使用 LongAdder 替代 AtomicLong 再替代 synchronized

七、总结

  1. synchronized 经历了从重量级到轻量级的蜕变。 锁升级机制让 synchronized 在低竞争场景下的性能几乎与无锁代码相当。理解偏向锁、轻量级锁、重量级锁的演进路径,是深入 JVM 性能调优的必备知识。

  2. AQS 是 JUC 的基石。 它通过 state + CLH 队列实现了可扩展的同步框架。独占模式、共享模式、条件变量的设计为上层同步器提供了统一的基础设施。

  3. 锁的选择需要权衡。 synchronized 足够简单且性能优异,适合 90% 的场景。但在需要超时、可中断、多条件变量、读写锁分离等高级特性时,AQS 的实现(ReentrantLock、ReadWriteLock 等)不可替代。

  4. 底层都依赖操作系统。 无论是 synchronized 最终膨胀为 mutex,还是 AQS 使用 LockSupport.park(),最终都会触发操作系统的线程调度。理解这一点是理解并发性能瓶颈的关键。

  5. 面向虚拟线程的新时代。 JDK 21 引入虚拟线程后,传统的”用线程池处理高并发”模式正在被”创建大量轻量级线程”所取代。在虚拟线程场景下,synchronized 是首选锁机制,因为它在虚拟线程的调度下表现更好。


Java 线程池核心原理与源码级解析

一、引言:线程池——Java 并发编程的基石

Java 并发编程中,线程池是使用最频繁、踩坑最深的组件之一。无论是 Web 服务器处理 HTTP 请求、RPC 框架处理远程调用,还是批处理任务调度,线程池都是承上启下的关键设施。

使用线程池的好处显而易见:降低资源开销(复用线程而非频繁创建销毁)、提高响应速度(任务提交后无需等待线程创建)、管理线程生命周期(统一的创建、销毁、监控)。但在实际项目中,因为线程池参数配置不当导致的生产事故比比皆是——OOM、CPU 打满、服务雪崩、死锁……这些问题的根源往往在于开发者没有真正理解 ThreadPoolExecutor 内部到底发生了什么。

本文不会停留在”阿里巴巴 Java 开发手册建议用 ThreadPoolExecutor 而非 Executors”这种表层建议上,而是深入 JDK 源码,逐层拆解 ThreadPoolExecutor 的 7 大参数如何协同工作,再给出经过实战检验的最佳实践。

1.1 Executors 的陷阱

// 反面教材:使用 Executors 创建线程池
// 这行代码在很多项目中赫然存在
ExecutorService executor = Executors.newFixedThreadPool(10);

// Executors.newFixedThreadPool 内部实现:
// return new ThreadPoolExecutor(10, 10,
//                               0L, TimeUnit.MILLISECONDS,
//                               new LinkedBlockingQueue());
// LinkedBlockingQueue 的默认容量是 Integer.MAX_VALUE(21亿+)
// 当任务提交速度超过处理速度时,队列无限制堆积 → OOM

// 同样的陷阱存在于:
// Executors.newSingleThreadExecutor()  // 也是无界队列
// Executors.newCachedThreadPool()      // 最大线程数 Integer.MAX_VALUE

这正是阿里巴巴规范背后的深层原因——不是 ThreadPoolExecutor 有问题,而是 Executors 的默认参数隐藏了巨大风险。理解每个参数的含义,才能针对业务场景做出合理配置。

二、ThreadPoolExecutor 的 7 大参数源码拆解

2.1 参数全景

public ThreadPoolExecutor(int corePoolSize,         // 核心线程数
                          int maximumPoolSize,      // 最大线程数
                          long keepAliveTime,       // 空闲线程存活时间
                          TimeUnit unit,            // 存活时间单位
                          BlockingQueue<Runnable> workQueue,  // 任务队列
                          ThreadFactory threadFactory,        // 线程工厂
                          RejectedExecutionHandler handler)   // 拒绝策略
flowchart TD
    subgraph 线程池生命周期
        S[提交任务] --> A{工作线程数 < corePoolSize?}
        A -->|| B[创建核心线程执行任务]
        A -->|| C[任务进入 workQueue]
        C --> D{队列已满?}
        D -->|| E[任务在队列中等待]
        D -->|| F{工作线程数 < maximumPoolSize?}
        F -->|| G[创建非核心线程执行任务]
        F -->|| H[执行拒绝策略]
    end

    B --> I[线程执行任务后]
    I --> J{队列中还有任务?}
    J -->|| K[从队列取任务继续执行]
    J -->|| L{当前线程数 > corePoolSize?}
    L -->|| M[等待 keepAliveTime 后销毁]
    L -->|| N[阻塞等待新任务]

七大参数的协调关系: 上述流程图呈现了 ThreadPoolExecutor 最核心的任务提交-执行-回收流水线。每个参数都在这个流程中扮演特定角色。下面逐一拆解。

2.2 corePoolSize——核心线程数

核心线程的”核”在于:即使空闲也不会被回收(除非设置了 allowCoreThreadTimeOut(true))。

// ThreadPoolExecutor 源码中关于核心线程的关键逻辑
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();

    int c = ctl.get();

    // 第一步:如果工作线程数 < 核心线程数,新建线程
    // 注意:即使其他核心线程空闲,也会新建线程
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))  // true = core
            return;
        c = ctl.get();
    }

    // 第二步:尝试入队
    if (isRunning(c) && workQueue.offer(command)) {
        // ... 入队成功后的双重检查
    }
    // 第三步:尝试创建非核心线程
    else if (!addWorker(command, false))  // false = non-core
        reject(command);  // 拒绝策略
}

最佳实践:核心线程数的估算

// CPU 密集型任务:核心线程数 ≈ CPU 核数 + 1
// IO 密集型任务:核心线程数 ≈ CPU 核数 * (1 + 等待时间/计算时间)
// 
// 网上广为流传的公式:N = Ncpu * Ucpu * (1 + W/C)
// 其中 W 是等待时间,C 是计算时间,Ucpu 是 CPU 利用率目标
// 
// 但是——这个公式在实践中容易让人产生"精确"的错觉。
// IO 等待时间 W 很难精确测量,因为涉及网络延迟、锁竞争等动态因素。

// 更好的做法:进行压测
// 1. 从较小的核心线程数开始(如 CPU 核数 * 2)
// 2. 逐步增大并监控吞吐量、平均响应时间、CPU 利用率
// 3. 找到拐点——即再增大线程数吞吐量不再提升的点

2.3 maximumPoolSize——最大线程数

最大线程数限定了线程池中最多可以同时存在的线程数量,包括核心线程和非核心线程。

// 关键源码:addWorker 方法中的数量检查
private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // ... 状态检查省略 ...

        for (;;) {
            int wc = workerCountOf(c);

            // 核心检查:是否超过容量上限 CAPACITY 或对应的核心/最大限制
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;  // 无法创建新线程
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // 重读
            if (runStateOf(c) != rs)
                continue retry;
        }
    }
    // ... 实际 Worker 创建 ...
}

corePoolSize vs maximumPoolSize 的协作:

flowchart LR
    subgraph 资源弹性伸缩
        CORE["corePoolSize\n⌄ 固定驻留线程数"]
        MAX["maximumPoolSize\n⌄ 峰值允许的最大线程数"]
        QUEUE["workQueue\n⌄ 缓冲任务"]
    end

    CORE -->|"任务激增→队列满→"| QUEUE
    QUEUE -->|"队列满→触发扩容"| MAX
    MAX -->|"任务回稳→空闲线程超时回收"| CORE

反面教材:maxPoolSize 设得过大

// 错误配置
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    10,                              // corePoolSize
    10000,                           // maximumPoolSize(太大!)
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000)  // 有界队列
);

// 问题:当 10000 个线程同时运行时,光是线程栈就占用
// 默认栈大小 1MB × 10000 ≈ 10GB 内存
// 加上线程切换的 CPU 开销,系统瞬间崩溃

最佳实践:maximumPoolSize 应该 > corePoolSize 但有限度

// 合理配置
int cpuCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    cpuCores * 2,                    // corePoolSize
    cpuCores * 4,                    // maximumPoolSize(上限控制)
    30L, TimeUnit.SECONDS,           // 存活时间
    new ArrayBlockingQueue<>(500),   // 有界队列
    new NamedThreadFactory("worker"),
    new CallerRunsPolicy()           // 拒绝策略
);

2.4 keepAliveTime——空闲线程存活时间

非核心线程在空闲了 keepAliveTime 后会被自动回收。这是线程池实现资源弹性的关键机制。

// Worker 线程的 run 方法循环
// getTask() 中使用 keepAliveTime 控制等待时间
final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock();  // 允许中断
    boolean completedAbruptly = true;
    try {
        // 核心循环:不断取任务执行
        while (task != null || (task = getTask()) != null) {
            // ... 执行任务 ...
        }
        completedAbruptly = false;
    } finally {
        // 线程退出时的清理
        processWorkerExit(w, completedAbruptly);
    }
}

private Runnable getTask() {
    boolean timedOut = false;

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // ... 状态检查 ...

        int wc = workerCountOf(c);

        // 关键判断:是否需要"计时等待"
        // timed = true 表示线程需要计时回收
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;  // 返回 null 导致 Worker 退出循环 → 线程销毁
            continue;
        }

        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();  // 核心线程阻塞等待
            if (r != null)
                return r;
            timedOut = true;  // poll 超时
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

这段源码揭示了几个重要结论:

场景 workerCount timed 行为
只有核心线程 wc <= corePoolSize false 核心线程调用 take() 阻塞等待,永不超时
有非核心线程 wc > corePoolSize true 非核心线程调用 poll(keepAliveTime) 等待
允许核心线程超时 allowCoreThreadTimeOut=true true 所有线程都使用计时等待

allowCoreThreadTimeOut 的两个极端:

// 默认:核心线程永远存活
// 适合:长期运行的服务器,需要稳定的线程池
executor.allowCoreThreadTimeOut(false);

// 启用核心线程超时
// 适合:任务波峰波谷明显的场景
// 如:业务低峰期只保留 0 个线程
executor.allowCoreThreadTimeOut(true);
executor.setKeepAliveTime(30, TimeUnit.SECONDS);

2.5 workQueue——任务队列

workQueue 是线程池中最容易被低估的参数。它决定了当核心线程都在忙时,任务如何缓冲

public interface BlockingQueue<E> extends Queue<E> {
    // 入队方法对比:
    boolean add(E e);     // 满了就抛异常 IllegalStateException
    boolean offer(E e);   // 满了就返回 false(不阻塞)
    void put(E e);       // 满了就阻塞等待
    boolean offer(E e, long timeout, TimeUnit unit); // 有限时间阻塞

    // 出队方法对比:
    E take();            // 队列空就阻塞等待
    E poll(long timeout, TimeUnit unit); // 有限时间等待
}

常用队列对比分析:

队列 容量 特点 适用场景 隐患
LinkedBlockingQueue 默认无界 链表结构,入队出队独立锁 任务相对均匀 队列堆积 → OOM
ArrayBlockingQueue 有界 数组结构,单锁 最常用(推荐) 需要合理设定容量
SynchronousQueue 0 不存任务,直接转交给线程 CachedThreadPool 线程创建失控
PriorityBlockingQueue 无界 按优先级出队 任务有优先级需求 同样 OOM
DelayedWorkQueue 无界 定时延迟 ScheduledThreadPool 用到定时场景
LinkedTransferQueue 无界 支持 CAS 操作 高吞吐 同样 OOM
// 核心逻辑:线程池如何与队列交互
public void execute(Runnable command) {
    // ...
    // 当工作线程达到 corePoolSize 后,尝试入队
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 双重检查:入队后如果线程池被关闭,从队列移除并执行拒绝策略
        if (!isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);  // 没有活跃线程也得加一个
    }
    // 入队失败(队列满)→ 尝试创建非核心线程
    else if (!addWorker(command, false))
        reject(command);
}

最佳实践:合理选择队列类型

// ✅ 推荐:ArrayBlockingQueue + 合理容量
int queueSize = 500;  // 根据任务处理能力和延迟要求决定
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(queueSize);

// 估算队列容量的方法:
// 队列容量 ≈ 平均任务处理时长(ms) × 期望 QPS × 安全系数
// 假设处理耗时 20ms,期望 QPS 2000,安全系数 2
// queueSize ≈ 20/1000 × 2000 × 2 = 80
// 取整:100

2.6 ThreadFactory——线程工厂

ThreadFactory 负责创建新线程。它的默认实现很简单——直接 new Thread(),但带来的问题是:

// 默认 ThreadFactory 的问题:
// 1. 线程名称无意义(如 "pool-1-thread-1")
// 2. 非守护线程,可能导致 JVM 无法退出
// 3. 无异常处理
// 4. 默认优先级(NORM_PRIORITY)

// ThreadPoolExecutor 中的默认实现:
static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
        namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-";
    }

    public Thread newThread(Runnable r) {
        return new Thread(r, namePrefix + threadNumber.getAndIncrement());
    }
}

最佳实践:自定义 ThreadFactory

public class NamedThreadFactory implements ThreadFactory {
    private final String namePrefix;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final boolean daemon;
    private final Thread.UncaughtExceptionHandler handler;

    public NamedThreadFactory(String name) {
        this(name, false, null);
    }

    public NamedThreadFactory(String name, boolean daemon, 
                              Thread.UncaughtExceptionHandler handler) {
        this.namePrefix = name + "-";
        this.daemon = daemon;
        this.handler = handler;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, namePrefix + threadNumber.getAndIncrement());
        t.setDaemon(daemon);
        if (handler != null) {
            t.setUncaughtExceptionHandler(handler);
        }
        return t;
    }
}

// 使用
executor = new ThreadPoolExecutor(
    10, 20, 30L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),
    new NamedThreadFactory("biz-worker", false, 
        (t, e) -> log.error("Thread {} died", t.getName(), e)),
    new CallerRunsPolicy()
);

2.7 RejectedExecutionHandler——拒绝策略

当线程池已满(达到 maximumPoolSize 且队列已满)时,拒绝策略决定如何处理新提交的任务。

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

JDK 内置 4 种拒绝策略:

flowchart TD
    S[队列满 + 线程满] --> R{选择哪种拒绝策略?}
    R --> A[AbortPolicy\n抛出 RejectedExecutionException]
    R --> B[CallerRunsPolicy\n在提交者线程中执行]
    R --> C[DiscardPolicy\n静默丢弃]
    R --> D[DiscardOldestPolicy\n丢弃队列中最旧的任务]

    A -->|优点: 明确定义| A1[缺点: 可能导致\n调用方异常]
    B -->|优点: 背压机制| B1[缺点: 提交者线程\n执行时间增长]
    C -->|优点: 不抛异常| C1[缺点: 任务丢了下\n都不知道]
    D -->|优点: 丢弃最老的| D1[缺点: 优先级高的\n可能被丢弃]
// 四种内置策略的源码核心逻辑:

// 1. AbortPolicy(默认)
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    throw new RejectedExecutionException("Task " + r.toString() + 
        " rejected from " + e.toString());
}

// 2. CallerRunsPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        r.run();  // 在提交者的线程中同步执行!
    }
}

// 3. DiscardPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    // 什么都不做——静默丢弃
}

// 4. DiscardOldestPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        e.getQueue().poll();  // 丢弃队列中最老的任务
        e.execute(r);         // 重新提交新任务
    }
}

最佳实践:生产环境推荐策略

// 方案 1: CallerRunsPolicy —— 隐式背压
// 适用于:对任务丢失零容忍的场景
// 效果:提交者线程执行任务 → 提交速度自然减慢 → 系统自动调节

// 方案 2: 自定义策略 —— 最灵活
public class RetryRejectedPolicy implements RejectedExecutionHandler {
    private static final int MAX_RETRIES = 3;
    private static final long RETRY_INTERVAL_MS = 100;

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            // 记录告警
            log.warn("Task rejected, queue={}, active={}, pool={}",
                     e.getQueue().size(), e.getActiveCount(), e.getPoolSize());
            // 尝试重新提交(可以放到 MQ/其他方式处理)
            RetryTask task = new RetryTask(r, MAX_RETRIES);
            scheduledExecutor.schedule(() -> {
                if (!e.isShutdown()) {
                    e.execute(task);
                }
            }, RETRY_INTERVAL_MS, TimeUnit.MILLISECONDS);
        }
    }
}

三、ctl——线程池的核心状态管理

ThreadPoolExecutor 用一个 AtomicIntegerctl)同时管理两个状态:线程池运行状态(高 3 位)和工作线程数量(低 29 位):

// 包级私有——核心控制状态
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

// COUNT_BITS = 29 (Integer.SIZE - 3)
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;  // 000 11111...111

// 运行状态存储在高 3 位
// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;  // 111
private static final int SHUTDOWN   =  0 << COUNT_BITS;  // 000
private static final int STOP       =  1 << COUNT_BITS;  // 001
private static final int TIDYING    =  2 << COUNT_BITS;  // 010
private static final int TERMINATED =  3 << COUNT_BITS;  // 011

// 状态变化流程
private static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

// 线程池状态变迁
// RUNNING → SHUTDOWN: 调用 shutdown(),不接受新任务
// RUNNING → STOP:     调用 shutdownNow(),中断所有线程
// SHUTDOWN → TIDYING: 队列为空且工作线程为 0
// STOP → TIDYING:      工作线程为 0
// TIDYING → TERMINATED: terminated() 钩子方法执行完毕
flowchart LR
    RUNNING -->|shutdown| SHUTDOWN
    RUNNING -->|shutdownNow| STOP
    SHUTDOWN -->|队列空且线程数=0| TIDYING
    STOP -->|线程数=0| TIDYING
    TIDYING -->|执行 terminated| TERMINATED

这种用单个 AtomicInteger 同时管理两个状态的技巧,使得 ThreadPoolExecutor 在判断”当前状态是否允许创建新线程”时只需要一次 CAS 操作,无需加锁。

四、Worker——线程池中的线程包装

每个真正执行任务的线程都被包装为一个 Worker

private final class Worker extends AbstractQueuedSynchronizer
    implements Runnable {

    final Thread thread;  // 实际执行任务的线程
    Runnable firstTask;   // 创建时指定的第一个任务(可能为 null)
    volatile long completedTasks;  // 已完成任务数

    Worker(Runnable firstTask) {
        setState(-1);  // 禁止中断直到 runWorker 开始
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }

    public void run() {
        runWorker(this);  // 委托给外部方法
    }

    // 继承 AQS 实现简单的互斥锁
    // 用于控制线程中断时的竞争条件
    // 0 表示未锁定,1 表示已锁定
    protected boolean tryAcquire(int unused) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }
}

Worker 继承 AQS 而不是 ReentrantLock,是为了实现不可重入的独占锁——这在中断线程时非常重要,确保不会在持有锁时被中断。

五、线程池监控——你的池子在干活吗?

5.1 内置监控指标

ThreadPoolExecutor 提供了多个可访问的监控指标:

public class ThreadPoolMonitor {

    public static void printMetrics(ThreadPoolExecutor executor, String name) {
        // 基础指标
        int poolSize = executor.getPoolSize();            // 当前线程数
        int activeCount = executor.getActiveCount();      // 活跃线程数
        int corePoolSize = executor.getCorePoolSize();    // 核心线程数
        int maximumPoolSize = executor.getMaximumPoolSize();
        long completedTaskCount = executor.getCompletedTaskCount();  // 完成数量
        long taskCount = executor.getTaskCount();         // 总任务数
        int queueSize = executor.getQueue().size();       // 队列堆积数
        int remainingCapacity = executor.getQueue().remainingCapacity();  // 剩余容量

        log.info("[{}] PoolSize={}, Active={}, Completed={}, Queue={}/{}, Core/Max={}/{}",
                 name, poolSize, activeCount, completedTaskCount,
                 queueSize, queueSize + remainingCapacity,
                 corePoolSize, maximumPoolSize);
    }
}

最佳实践:线程池监控的黄金指标

指标 正常范围 告警阈值 含义
activeCount < corePoolSize > corePoolSize * 0.8 核心线程可能不够
queueSize 接近 0 > queueCapacity * 0.7 任务处理能力不足
completedTaskCount 持续增长 长时间不增长 线程池阻塞或死锁
poolSize = corePoolSize > corePoolSize 触发了临时扩容
rejectedCount 0 > 0 系统过载
longestTaskTime < 1s > 10s 有任务长时间阻塞

5.2 动态调整线程池参数

ThreadPoolExecutor 支持运行时动态调整参数——这是很多人不知道但非常实用的特性:

public class DynamicThreadPool {
    private final ThreadPoolExecutor executor;

    public DynamicThreadPool() {
        this.executor = new ThreadPoolExecutor(
            10, 20, 30L, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100),
            new NamedThreadFactory("dynamic"),
            new CallerRunsPolicy()
        );

        // 启动调整线程
        scheduleAdjustment();
    }

    // 根据监控数据动态调整
    public void adjustPoolSize(int newCore, int newMax) {
        executor.setCorePoolSize(newCore);
        executor.setMaximumPoolSize(newMax);
    }

    // 自动调节:根据队列深度调整
    private void scheduleAdjustment() {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        scheduler.scheduleAtFixedRate(() -> {
            int queueSize = executor.getQueue().size();
            int activeCount = executor.getActiveCount();

            if (queueSize > 100 && activeCount >= executor.getCorePoolSize()) {
                // 队列堆积,需要扩容
                executor.setCorePoolSize(executor.getCorePoolSize() + 2);
                executor.setMaximumPoolSize(executor.getMaximumPoolSize() + 2);
                log.info("Thread pool expanded: core={}, max={}",
                         executor.getCorePoolSize(), executor.getMaximumPoolSize());
            } else if (queueSize == 0 && executor.getPoolSize() > 10) {
                // 任务很少,缩容
                int newCore = Math.max(4, executor.getCorePoolSize() - 1);
                executor.setCorePoolSize(newCore);
                log.info("Thread pool shrunk: core={}", newCore);
            }
        }, 10, 30, TimeUnit.SECONDS);
    }
}

六、常见线程池陷阱与实战案例

6.1 陷阱:线程池中的异常吞没

// ❌ 反例:线程池中的异常被吞没
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> {
    // 这个异常不会传播出来
    throw new RuntimeException("Something went wrong!");
});
// submit() 的异常被包装在 Future 中
// 如果不调用 Future.get(),异常永远不会被知道!

// ✅ 正例 1:使用 execute() 而不是 submit()
executor.execute(() -> {
    throw new RuntimeException("This will be caught by handler!");
});

// ✅ 正例 2:捕获异常并记录
executor.execute(() -> {
    try {
        doRiskyWork();
    } catch (Exception e) {
        log.error("Task failed", e);
        // 自定义处理
    }
});

// ✅ 正例 3:设置 UncaughtExceptionHandler
ThreadFactory factory = r -> {
    Thread t = new Thread(r);
    t.setUncaughtExceptionHandler((thread, e) -> 
        log.error("Thread {} died with exception", thread.getName(), e));
    return t;
};

6.2 陷阱:任务依赖导致饥饿死锁

// ❌ 反例:单线程池中的任务依赖
ExecutorService executor = new ThreadPoolExecutor(
    1, 1, 0L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>()
);

// 任务 A 提交任务 B
Future<String> future = executor.submit(() -> {
    // 任务 A
    log.info("Task A started");
    // 等待任务 B 的结果
    Future<String> bResult = executor.submit(() -> "B");  // 🔴 死锁!
    return "A" + bResult.get();  // 永远等不到,因为线程池只有一个线程
});

// 执行结果:死锁!线程池中唯一的工作线程被任务 A 占用
// 任务 B 永远无法获得线程资源

解决方案: 识别依赖关系,使用不同的线程池,或者使用 CompletableFuture 避免阻塞等待。

6.3 陷阱:线程池关闭不当

// ❌ 反例:直接 shutdownNow() 导致任务丢失
executor.shutdownNow();  // 中断所有线程,返回未执行的任务列表
// 返回值中的任务如果不处理,就永远丢失了

// ✅ 正例:优雅关闭
public void gracefulShutdown(ExecutorService executor, long timeout, TimeUnit unit) {
    executor.shutdown();  // 不再接受新任务,等待已有任务完成
    try {
        if (!executor.awaitTermination(timeout, unit)) {
            executor.shutdownNow();  // 超时后强制关闭
            if (!executor.awaitTermination(timeout, unit)) {
                log.error("Executor did not terminate");
            }
        }
    } catch (InterruptedException e) {
        executor.shutdownNow();
        Thread.currentThread().interrupt();
    }
}

6.4 陷阱:ThreadLocal 与线程复用

// ❌ 反例:线程池中使用 ThreadLocal 不清理
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
    final int value = i;
    executor.execute(() -> {
        threadLocal.set(value);
        doWork();
        // 这里没有 remove()!
        // 下一个使用这个线程的任务会读到错误的值
    });
}

// ✅ 正例:用完立即清理
executor.execute(() -> {
    try {
        threadLocal.set(value);
        doWork();
    } finally {
        threadLocal.remove();  // 必须清理!
    }
});

七、总结

  1. ThreadPoolExecutor 的 7 大参数是一个有机整体。 corePoolSize 决定基础并发量,workQueue 承载缓冲,maximumPoolSize 控制峰值弹性,keepAliveTime 管理资源释放,ThreadFactory 提供线程信息,RejectedExecutionHandler 处理过载。任何参数的错误配置都可能引发严重问题。

  2. 活用有界队列 + CallerRunsPolicy。 这组组合是生产环境最推荐的配置。有界队列防止 OOM,CallerRunsPolicy 提供隐式背压机制。

  3. 理解 ctl 的设计哲学。 一个 AtomicInteger 同时管理状态和线程数,用位运算实现无锁的状态判断,是 JDK 源码中”用复杂技巧解决简单问题”的精妙案例。

  4. 善用监控。 线程池的黑盒特性决定了必须持续监控活跃线程数、队列深度、拒绝次数等指标。这些指标是定位性能问题的最直接线索。

  5. 警惕线程池陷阱。 异常吞没、任务依赖死锁、ThreadLocal 污染、关闭不当——这些是生产环境最常遇到的问题,需要在代码中预设保护机制。

  6. 动态调整让你的线程池”活起来”。 ThreadPoolExecutor 提供了运行时调整参数的接口,结合监控数据可以实现自动伸缩。


Java 并发核心机制深入拆解:从 synchronized 到 AQS 的底层真相


title: “Java 并发核心机制深入拆解:从 synchronized 到 AQS 的底层真相”

一、引言

并发编程是 Java 开发者绕不开的核心能力。从最简单的 synchronized 关键字到复杂的 ReentrantLockThreadLocal,每一层抽象背后都隐藏着操作系统、内存模型和 CPU 架构的深刻考量。本文将从字节码与源码两个维度,逐层拆解 Java 并发体系的五大核心机制。

二、synchronized 的原理与优化演进

2.1 字节码层面的真相

synchronized 在 Java 代码中有三种使用形态:

// 1. 修饰实例方法
public synchronized void instanceMethod() { }
// 2. 修饰静态方法
public static synchronized void staticMethod() { }
// 3. 修饰代码块
public void blockMethod() {
    synchronized (this) { }
}

通过 javap -v 反编译,三种形态的字节码表现完全不同:

实例方法(ACC_SYNCHRONIZED 标记):

public synchronized void instanceMethod();
  flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
    stack=0, locals=1, args_size=1
    0: return

代码块(monitorenter/monitorexit):

 3: monitorenter
 4: aload_1
 5: monitorexit
 6: goto 14
 9: astore_2
10: aload_1
11: monitorexit
12: athrow

关键区别在于:方法级的 synchronized 通过方法标志位隐式实现,而代码块级则依赖显式的 monitorentermonitorexit 指令。无论哪种形式,最终都依赖于底层 ObjectMonitor 机制。

2.2 锁升级:从无锁到重量级

JDK 1.6 之后,synchronized 经历了从”重量级”到”自适应”的重大优化,形成了无锁 → 偏向锁 → 轻量级锁 → 重量级锁的升级路径:

flowchart LR
    A[无锁] -->|"线程获取锁"| B[偏向锁]
    B -->|"竞争出现"| C[轻量级锁]
    C -->|"自旋失败/竞争加剧"| D[重量级锁]
    D -->|"锁释放"| A
    style A fill:#4CAF50,color:white
    style B fill:#2196F3,color:white
    style C fill:#FF9800,color:white
    style D fill:#f44336,color:white

偏向锁(Biased Locking): 锁对象头中的 Mark Word 记录持有锁的线程 ID。单线程重复获取时无需 CAS 操作,延迟极低。JDK 15 开始默认禁用,JDK 21 已移除。

轻量级锁(Lightweight Lock): 通过 CAS 在对象头 Mark Word 中记录指向当前线程栈帧中 Lock Record 的指针。不涉及操作系统内核切换,通过自旋等待。

重量级锁(Heavyweight Lock): 当自旋超过阈值(默认 10 次,可自适应调整),锁膨胀为重量级锁,Mark Word 指向 ObjectMonitor 对象,线程被阻塞在 _WaitSet_EntryList 上,需要操作系统内核态切换(pthread_mutex_lock)。

flowchart TD
    subgraph Mark Word 结构
        M[32/64  Mark Word]
        M --> M1[偏向锁: 线程ID + Epoch + 分代年龄 + 偏向位]
        M --> M2[轻量锁: 指向 Lock Record 的指针]
        M --> M3[重量锁: 指向 ObjectMonitor 的指针]
        M --> M4[GC 标记: 标记阶段专用]
    end

2.3 锁升级的性能对比

锁状态 CAS 开销 内核切换 适用场景 延迟
偏向锁 0 0 单线程重复获取 <10ns
轻量级锁 1次CAS 0 低竞争、短持有 ~50ns
重量级锁 多次CAS 1~2次 高竞争、长持有 ~10μs

反直觉的结论: 重量级锁在极度高竞争下反而比 CAS-heavy 的轻量级锁更高效,因为线程被挂起后不会浪费 CPU 自旋。

三、AQS 框架与 ReentrantLock 实现

3.1 AQS 核心架构

AbstractQueuedSynchronizer(AQS)是 JUC 锁与同步器的基石。ReentrantLockCountDownLatchSemaphoreReentrantReadWriteLock 均基于它实现。

// AQS 核心字段
public abstract class AbstractQueuedSynchronizer {
    // 同步状态,volatile 保证可见性
    private volatile int state;

    // CLH 等待队列的头尾节点
    private transient volatile Node head;
    private transient volatile Node tail;
}

AQS 的设计精髓在于 模板方法模式。子类只需实现 tryAcquire/tryRelease(独占模式)或 tryAcquireShared/tryReleaseShared(共享模式),而队列管理、阻塞唤醒、中断处理等复杂逻辑由 AQS 统一完成。

3.2 CLH 队列与 Node 节点

AQS 内部维护了一个 CLH(Craig, Landin, Hagersten)锁的变体——显式队列 + 自旋 + 阻塞:

static final class Node {
    // 节点模式:共享/独占
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;

    // 等待状态:CANCELLED=1, SIGNAL=-1, CONDITION=-2, PROPAGATE=-3
    volatile int waitStatus;

    volatile Node prev;    // 前驱
    volatile Node next;    // 后继
    volatile Thread thread; // 关联线程
    Node nextWaiter;        // 条件队列的下一节点
}

关键机制流程:

flowchart TD
    A[尝试获取锁 tryAcquire] -->|成功| B[执行业务逻辑]
    A -->|失败| C[创建 Node 加入 CLH 队列]
    C --> D[自旋检查前驱是否为 head]
    D -->|是且 tryAcquire 成功| E[设为新 head 执行业务]
    D -->|| F[parkAndCheckInterrupt 阻塞]
    F --> G[被前驱线程 unpark 唤醒]
    G --> D
    E --> H[释放锁 setState+unpark 后继]

3.3 ReentrantLock 实现细节

public class ReentrantLock implements Lock {
    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {
        // 非公平锁:直接 CAS 抢一次
        // 公平锁:hasQueuedPredecessors() 才 CAS
    }

    static final class NonfairSync extends Sync {
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1); // 进入 AQS
        }
    }

    static final class FairSync extends Sync {
        final void lock() {
            acquire(1); // 直接进 AQS,无插队
        }
    }
}

公平锁 vs 非公平锁的核心差异:

特性 非公平锁 公平锁
首次获取 直接 CAS 抢锁 检查队列是否有等待者
吞吐量 更高(减少线程切换) 较低
公平性 可能”插队”导致饥饿 严格 FIFO
适用场景 大部分业务场景 对公平性敏感的金融交易

反直觉的点: 非公平锁虽然不公平,但整体吞吐量更高。因为唤醒一个线程需要上下文切换,而正在运行的线程直接获取锁可以省去这次切换,让刚释放锁的线程继续运行。

3.4 Condition 的实现

Conditionsynchronizedwait/notify 对应,但更灵活:

class BoundedBuffer {
    final Lock lock = new ReentrantLock();
    final Condition notFull  = lock.newCondition();
    final Condition notEmpty = lock.newCondition();

    void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length) notFull.await();
            // ... 插入数据
            notEmpty.signal();
        } finally { lock.unlock(); }
    }
}

Condition.await() 将当前线程从 AQS 同步队列移到 条件队列firstWaiter/lastWaiter 单向链表),释放锁并挂起。被 signal() 后,线程从条件队列取出,重新加入到同步队列竞争锁。

四、volatile 与 happens-before 规则

4.1 volatile 的语义

volatile 是 Java 提供的最轻量级的同步机制,具备两个核心语义:

  1. 可见性: 对一个 volatile 变量的写操作,后续对其他线程的读操作立即可见
  2. 禁止重排序: volatile 读写操作前后不能重排序
// volatile 的典型使用场景:状态标志
volatile boolean running = true;

// 线程1
public void stop() {
    running = false;  // volatile 写 → StoreLoad 屏障
}

// 线程2
public void run() {
    while (running) { // volatile 读
        // 业务逻辑
    }
}

4.2 内存屏障的实现

在 x86 架构下,volatile 写会在指令前插入 StoreStore 屏障(防止前面的普通写被重排序到 volatile 写之后),在指令后插入 StoreLoad 屏障(防止后面的 volatile 读/写被重排序到 volatile 写之前)。

flowchart TD
    subgraph "正常代码顺序"
        A[普通写操作]
        B[volatile 写]
        C[普通读操作]
    end
    subgraph "内存屏障效果"
        D[普通写操作]
        E[StoreStore 屏障]
        F[volatile 写]
        G[StoreLoad 屏障]
        H[普通读操作]
    end

4.3 happens-before 规则

JMM(Java Memory Model)通过 happens-before 规则定义多线程操作的 偏序关系。如果 A happens-before B,则 A 的操作结果对 B 可见:

  1. 程序顺序规则: 单线程内,前面的操作 happens-before 后续操作
  2. 监视器锁规则: 解锁操作 happens-before 后续加锁
  3. volatile 规则: volatile 写 happens-before 后续读
  4. 传递性: A happens-before B,B happens-before C → A happens-before C
  5. 线程启动规则: Thread.start() happens-before 该线程的任何操作
  6. 线程中断规则: interrupt() happens-before 检测到中断
  7. 线程终止规则: join() 返回前的所有操作 happens-before join 之后
// 经典案例:没有 happens-before 的错误代码
int a = 0, b = 0;
boolean flag = false;

// 线程1
a = 1;
b = 2;
flag = true;   // volatile 写

// 线程2
if (flag) {     // volatile 读
    // a 和 b 一定可见,因为 volatile 写 happens-before volatile 读
    assert a == 1 && b == 2; // 永远为 true
}

五、ThreadLocal 源码深度分析

5.1 数据结构

ThreadLocal 并不直接存储值,而是以 ThreadLocalMap 的形式存储在 Thread 对象上:

// Thread 类中的字段
public class Thread {
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
flowchart LR
    T1[Thread-1] --> M1[ThreadLocalMap]
    M1 --> E1[Entry: key=TL1, value=v1]
    M1 --> E2[Entry: key=TL2, value=v2]
    T2[Thread-2] --> M2[ThreadLocalMap]
    M2 --> E3[Entry: key=TL1, value=v3]
    M2 --> E4[Entry: key=TL2, value=v4]

不用 Map 而是用 Entry[] 的原因: ThreadLocalMap 使用开放地址法(线性探测)而非拉链法解决哈希冲突,因为 Entry 数量通常很小(大多数线程只有少数几个 ThreadLocal 变量),开放地址法缓存更友好。

5.2 弱引用与内存泄漏

每个 Entry 的 key 是 弱引用(WeakReference)

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal> {
        Object value;
        Entry(ThreadLocal k, Object v) {
            super(k); // 弱引用
            value = v;
        }
    }
}

为什么要用弱引用? 如果 key 是强引用,ThreadLocal 对象在线程存活期间永远不会被 GC,造成内存泄漏。弱引用允许 ThreadLocal 被回收,此时 Entry 的 key 变为 null。

状态演进:
① ThreadLocal 强引用 → Entry key 弱引用 → value 强引用
② ThreadLocal 不再使用(=null)后
③ GC 回收 ThreadLocal → Entry.key = null
④ value 仍然存在 → **内存泄漏!**

正确的使用方式: 每次使用完毕调用 remove() 方法,或者在 finally 块中清理。

// 正确用法
ThreadLocal<Connection> conn = new ThreadLocal<>();
try {
    conn.set(dataSource.getConnection());
    // ... 操作
} finally {
    conn.remove(); // 务必清理
}

5.3 哈希算法

ThreadLocalMap 使用 斐波那契哈希(黄金分割数)来生成散列值:

public class ThreadLocal<T> {
    private final int threadLocalHashCode = nextHashCode();

    private static AtomicInteger nextHashCode =
        new AtomicInteger();
    private static final int HASH_INCREMENT = 0x61c88647; 
    // 黄金分割数

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
}

0x61c88647 是 2^32 的黄金比例对应的整数。利用这个魔数生成的哈希值在 2 的幂次方的数组中几乎均匀分布,且相邻 ThreadLocal 的哈希值间隔固定,减少了线性探测的冲突。

5.4 InheritableThreadLocal

InheritableThreadLocal 允许子线程继承父线程的 ThreadLocal 值。实现原理是在 Thread.init() 创建子线程时,将父线程的 inheritableThreadLocals 复制到子线程:

private void init(ThreadGroup g, Runnable target, ...) {
    // ...
    if (parent.inheritableThreadLocals != null) {
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(
                parent.inheritableThreadLocals);
    }
}

注意: 对于线程池场景,InheritableThreadLocal 会失效(线程复用不会重新继承),需要使用 TransmittableThreadLocal(阿里开源的 TTL)。

六、总结

机制 核心特点 适用场景 注意事项
synchronized 自动管理、锁升级、简洁 短操作、低竞争 不能超时,阻塞不可中断
ReentrantLock 可中断、可超时、公平可选 长操作、高竞争 手动 unlock,需 finally
volatile 轻量级、无阻塞 状态标志、double-check 不保证原子性
ThreadLocal 线程隔离、无竞争 上下文传递、连接池 必须 remove,防泄漏

Java 并发体系的精妙之处在于:上层语法简单(synchronized),下层实现复杂(锁升级);上层语义明确(volatile 可见性),下层硬件屏障支撑;上层抽象易用(AQS),下层实现灵活(模板方法)。 理解这些底层机制是成为 Java 高级开发者的必经之路。


锁升级与降级机制

锁升级与降级机制

概述

在 MySQL InnoDB 中,锁的升级(Lock Escalation)和降级(Lock Degradation)指的是锁粒度范围的变化。与某些数据库(如 SQL Server)明确的锁升级机制不同,InnoDB 的锁升级/降级更多是自动优化行为,根据查询条件、索引使用情况和隔离级别自动调整锁的策略。

InnoDB 的锁升级

1. 记录锁升级为 Next-Key Lock

REPEATABLE READ 级别下,InnoDB 会对范围查询自动升级锁粒度:

-- 表:user(id INT PRIMARY KEY, age INT, INDEX idx_age(age))

-- 查询所有 age > 20 的记录
SELECT * FROM user WHERE age > 20 FOR UPDATE;

-- 加的锁:
-- ① 对每条符合条件的记录加 Record Lock
-- ② 对记录之间的间隙加 Gap Lock
-- ③ 组合成 Next-Key Lock

-- 范围:(-∞, 20] → (20, 30] → (30, +∞) 都上锁

这就是自动升级:从”只锁某行”升级到”锁定某个范围”。

2. 无索引时的全表扫描锁

当 WHERE 条件无法使用索引时,InnoDB 会进行全表扫描,看起来像是”锁升级”为全表范围:

-- ❌ name 无索引,全表扫描加锁
SELECT * FROM user WHERE name = 'Alice' FOR UPDATE;
-- InnoDB 对聚簇索引的每一条记录都加锁
-- 虽然没有使用"表锁",但效果类似锁全表

-- ✅ 有索引时,只锁精确匹配的行
CREATE INDEX idx_name ON user(name);
SELECT * FROM user WHERE name = 'Alice' FOR UPDATE;
-- 只锁住 Alice 对应的记录

3. 插入意向锁与间隙锁升级

当两个事务要对同一间隙插入数据时,插入意向锁会等待间隙锁释放。如果间隙锁范围很大,可能导致较多的阻塞。

-- 事务 A(持有间隙锁)
SELECT * FROM user WHERE age BETWEEN 20 AND 30 FOR UPDATE;
-- 锁了 (20,30) 的间隙

-- 事务 B(插入意向锁等待)
INSERT INTO user(age) VALUES(25);
-- 等待事务 A 释放 (20,30) 的间隙锁

InnoDB 的锁降级

1. 唯一索引等值查询降级

在 REPEATABLE READ 下,当使用唯一索引进行等值查询时,Next-Key Lock 会降级为 Record Lock:

-- id 是 PRIMARY KEY(唯一索引)
-- 等值查询时,Next-Key Lock 降级为 Record Lock

SELECT * FROM user WHERE id = 1 FOR UPDATE;
-- 在 RR 级别下:
-- 如果不是唯一索引:会加 Next-Key Lock(记录锁 + 间隙锁)
-- 如果是唯一索引:降级为 Record Lock(只锁 id=1 这一行)

-- 原因:唯一索引保证了不会有其他记录落在 id=1 的间隙中
-- 不需要 Gap Lock 来防止幻读

降级条件

-- ✅ 唯一索引等值查询 → Record Lock(降级)
SELECT * FROM user WHERE id = 1 FOR UPDATE;

-- ⚠️ 唯一索引等值查询未命中 → Gap Lock(加间隙锁)
SELECT * FROM user WHERE id = 100 FOR UPDATE;
-- id=100 的记录不存在,只在 (上一个ID, 100] 加间隙锁

-- ❌ 范围查询 → Next-Key Lock(不降级)
SELECT * FROM user WHERE id > 1 FOR UPDATE;

-- ❌ 二级索引等值查询 → Next-Key Lock(不降级)
SELECT * FROM user WHERE age = 25 FOR UPDATE;
-- age 是二级索引,RR 下保持 Next-Key Lock

2. READ COMMITTED 下的锁降级

从 REPEATABLE READ 切换到 READ COMMITTED 级别时,所有当前读的锁都会降级

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 原来的 RR 级别:Next-Key Lock(Record + Gap)
-- RC 级别:降级为 Record Lock

SELECT * FROM user WHERE age > 20 FOR UPDATE;
-- RR:锁住年龄 > 20 的所有行 + 间隙
-- RC:只锁住满足条件的记录,不锁间隙

比较:与 SQL Server 的区别

数据库 锁升级行为
SQL Server 明确的行锁→页锁→表锁升级机制
MySQL InnoDB 无表锁升级,但通过 Next-Key Lock 和索引使用情况自动调整范围
MySQL MyISAM 只有表锁(默认升级到表锁)

InnoDB 没有 SQL Server 式的”行锁超过阈值自动升级为表锁”的机制,其锁升级更多体现为锁范围的自然扩大。

实际应用中的锁优化

通过索引实现锁降级

-- 场景:根据订单状态查询并锁定
-- 如果 status 没有索引:
SELECT * FROM order WHERE status = 'PAID' FOR UPDATE;
-- 全表扫描加锁,开销大

-- 给 status 加索引:
CREATE INDEX idx_status ON order(status);
SELECT * FROM order WHERE status = 'PAID' FOR UPDATE;
-- 只锁住 status='PAID' 的记录

通过隔离级别实现锁降级

-- 如果业务允许低隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT * FROM order WHERE amount > 100 FOR UPDATE;
-- RR:Next-Key Lock
-- RC:Record Lock(降级)

面试要点

  1. InnoDB 无真正的锁升级:不像 SQL Server 有行→页→表的自动升级机制
  2. 唯一索引降级:唯一索引等值查询时,Next-Key Lock 降级为 Record Lock
  3. 隔离级别影响:RC 下所有当前读都使用 Record Lock(相对 RR 的降级)
  4. 无索引的后果:看起来像”升级”为全表加锁
  5. 面试考点:”InnoDB 会锁升级吗?”→ 没有 SQL Server 式的锁升级,但有 Next-Key 的加锁策略优化

innodb_lock_wait_timeout 详解

innodb_lock_wait_timeout 详解

概述

innodb_lock_wait_timeout 是 MySQL InnoDB 的一个重要配置参数,用于控制事务等待锁的超时时间。当事务因锁冲突而等待其他事务释放锁时,如果等待时间超过该阈值,InnoDB 会回滚出错的语句(注意不是整个事务),并返回超时错误。

基本概念

官方定义

innodb_lock_wait_timeout:
- 默认值:50 秒
- 最小值:1 秒
- 最大值:1073741824 秒
- 动态配置:是(SET GLOBAL / SET SESSION)

作用

当事务 A 需要获取某行数据的锁,但该锁被事务 B 持有时,事务 A 进入等待状态:

事务 A:尝试获得锁 → 锁被事务 B 持有 → 开始等待
                                           │
                                    innodb_lock_wait_timeout
                                           │
                    ╔═══════════════════════╧═══════════════════╗
                    ║                                           ║
             事务 B 释放锁                            事务 B 未释放锁
                    │                                           │
             事务 A 获得锁                        等待超时,事务 A 收到错误:
             继续执行                            ERROR 1205 (HY000): Lock wait
                                                timeout exceeded

与死锁检测的关系

innodb_lock_wait_timeoutinnodb_deadlock_detect 是两个独立但相关的机制:

机制 触发条件 行为 错误码
死锁检测 检测到循环等待(环) 选择牺牲事务回滚 1213
锁等待超时 等待锁的时间超过阈值 回滚出错的语句 1205

死锁检测关闭时innodb_deadlock_detect = OFF),只能依赖锁等待超时来避免无限阻塞。

配置方式

全局级别

-- 查看当前值
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
-- 或
SELECT @@global.innodb_lock_wait_timeout;

-- 修改全局值(影响新连接)
SET GLOBAL innodb_lock_wait_timeout = 10;

-- MySQL 8.0 持久化
SET PERSIST innodb_lock_wait_timeout = 10;

会话级别

-- 仅影响当前会话
SET SESSION innodb_lock_wait_timeout = 5;

-- 查看会话值
SELECT @@session.innodb_lock_wait_timeout;

配置文件(持久化)

[mysqld]
innodb_lock_wait_timeout = 10

不同业务的合理配置

高并发 OLTP 系统

-- 建议:5-10 秒
SET GLOBAL innodb_lock_wait_timeout = 5;

原因:OLTP 事务通常很快(毫秒级),长时间等待意味着大概率发生了死锁或有问题。及早放弃比长时间等待更优。

批量处理/Batch 系统

-- 建议:30-60 秒
SET GLOBAL innodb_lock_wait_timeout = 60;

原因:批量操作可能涉及大量行,锁持有时间较长,超时需要设高些。

低并发、对一致性要求高的系统

-- 建议:使用默认值 50 秒,或更高
SET GLOBAL innodb_lock_wait_timeout = 50;

锁等待超时的影响

超时回滚的范围

重要:innodb_lock_wait_timeout 超时后,默认只回滚出错的语句(statement),而不是整个事务。

START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;  -- 成功
UPDATE account SET balance = balance + 100 WHERE id = 2;  -- ❌ 锁等待超时!
-- ⚠️ 第一条 UPDATE 仍然有效!
-- 只有第二条 UPDATE 被回滚

COMMIT;
-- 后果:id=1 少了 100,id=2 没有增加 100 → 数据不一致

修正方案

-- 设置事务回滚策略
-- MySQL 8.0:
SET SESSION transaction_rollback_on_timeout = ON;
-- 或应用程序检测到超时后主动 ROLLBACK

应用程序如何处理

try:
    cursor.execute("BEGIN")
    cursor.execute("UPDATE account SET balance = balance - 100 WHERE id = 1")
    cursor.execute("UPDATE account SET balance = balance + 100 WHERE id = 2")
    connection.commit()
except pymysql.err.OperationalError as e:
    if e.args[0] == 1205:  # Lock wait timeout
        connection.rollback()  # ✅ 主动回滚整个事务
        print("超时回滚,需要重试")

监控锁等待

查看当前锁等待信息

-- 查看哪些事务正在等待锁
SELECT * FROM sys.innodb_lock_waits;
-- 或
SELECT * FROM performance_schema.data_lock_waits;

-- 查看等待时间
SELECT 
    r.trx_id AS waiting_trx_id,
    r.trx_mysql_thread_id AS waiting_thread,
    TIMESTAMPDIFF(SECOND, r.trx_started, NOW()) AS wait_seconds,
    b.trx_id AS blocking_trx_id,
    b.trx_mysql_thread_id AS blocking_thread
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id;

主动终止阻塞事务

-- 找到阻塞源头的线程 ID
SELECT trx_mysql_thread_id 
FROM information_schema.innodb_trx 
WHERE trx_state = 'RUNNING';

-- 杀掉阻塞的事务
KILL 12345;  -- 12345 是 trx_mysql_thread_id

实际调优案例

案例:高并发订单系统

问题:大量锁等待超时错误。

分析

-- 查看锁等待情况
SHOW GLOBAL STATUS LIKE 'Innodb_row_lock_current_waits';
-- 值很高,说明锁竞争激烈

方案

-- 1. 调低锁等待超时,快速失败减少堆积
SET GLOBAL innodb_lock_wait_timeout = 3;

-- 2. 检查慢事务
SELECT * FROM information_schema.innodb_trx 
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 5;

-- 3. 优化 SQL 和索引,减少锁持有时间

面试要点

  1. 默认值:50 秒,可以动态调整
  2. 与死锁检测的区别:锁等待超时是时间阈值触发,死锁检测是图算法触发
  3. 回滚范围:只回滚超时语句,不是整个事务 —— 需要应用程序额外处理
  4. 配置建议:OLTP 系统设置 5-10 秒,批量处理系统设置 30-60 秒
  5. 监控方法sys.innodb_lock_waitsinformation_schema + performance_schema

间隙锁(Gap Lock)的工作机制

间隙锁(Gap Lock)的工作机制

概述

间隙锁(Gap Lock)是 InnoDB 在 REPEATABLE READ 级别下为了解决幻读问题而引入的一种特殊锁。与记录锁锁定具体行不同,间隙锁锁定的是索引记录之间的间隙(gap),阻止其他事务在这些间隙中插入新记录。

间隙锁的本质

它锁的是什么?

间隙锁锁定的是索引记录之间的间隔(间隙),而不是具体的记录本身。

假设表中有以下数据和索引:

索引值:   1    3    5    7    9
对应的间隙:|---|------|------|------|----|
            (1,3) (3,5) (5,7) (7,9) (9,+∞)

间隙锁可以锁定:
– 两条索引记录之间的间隙
– 第一条索引记录之前的间隙
– 最后一条索引记录之后的间隙

间隙锁的特点

  1. 不锁定记录本身:间隙锁只阻止插入,不阻止对已有记录的修改
  2. 锁的是间隙范围:阻止其他事务在该间隙中插入任何行
  3. 间隙锁可以共存:多个事务可以对同一个间隙加 S 或 X 间隙锁,互不冲突。间隙锁的唯一作用是阻止插入

间隙锁的触发条件

1. 范围查询中使用当前读

-- 假设表中已有 id=1,3,5 的记录
BEGIN;
SELECT * FROM users WHERE id > 3 FOR UPDATE;
-- 锁定的间隙:
-- (3, 5]:Next-Key Lock
-- (5, +∞):间隙锁
-- 其他事务无法插入 id=4 或 id=6 的行

2. 非唯一索引的等值查询

-- age 是非唯一索引
SELECT * FROM users WHERE age = 25 FOR UPDATE;
-- 锁定 age=25 的索引记录以及左右相邻的间隙
-- 防止其他事务插入 age=25 的新行

3. 记录不存在时的锁

SELECT * FROM users WHERE id = 4 FOR UPDATE;
-- 如果 id=4 不存在
-- 锁定 (3, 5) 这个间隙(3 和 5 是相邻的已存在记录)

间隙锁的加锁范围

等值查询

-- 表:id(主键) 有 1, 3, 5

-- 锁定存在的行
SELECT * FROM users WHERE id = 3 FOR UPDATE;
-- 只锁 id=3 的记录(记录锁),不加间隙锁

-- 锁定不存在的行
SELECT * FROM users WHERE id = 4 FOR UPDATE;
-- 锁定间隙 (3, 5)

范围查询

-- 表:id 有 1, 3, 5

SELECT * FROM users WHERE id > 3 AND id < 7 FOR UPDATE;
-- 锁定的范围:
-- (3, 5]:Next-Key Lock(间隙锁 + 记录锁)
-- (5, 7):间隙锁(id=7 不存在,直接锁到上界)

非唯一索引

非唯一索引的等值查询也会加间隙锁:

-- age 是非唯一索引,有 20, 25, 30

SELECT * FROM users WHERE age = 25 FOR UPDATE;
-- 锁定的范围:
-- (20, 25]:Next-Key Lock
-- (25, 30):间隙锁
-- 这是为了防止幻读——阻止其他事务插入 age=25 的新行

间隙锁在不同隔离级别下的行为

REPEATABLE READ(默认)

-- 间隙锁生效
SELECT * FROM orders WHERE amount > 100 FOR UPDATE;
-- 会锁住 amount 索引上的记录和间隙

READ COMMITTED

-- 间隙锁不生效(除了外键约束和唯一键检查时)
SELECT * FROM orders WHERE amount > 100 FOR UPDATE;
-- 只锁匹配的行记录,不锁间隙

间隙锁带来的问题

1. 阻塞插入

事务 A:
  SELECT * FROM users WHERE id > 3 FOR UPDATE;
  -- 锁住 (3, +∞)

事务 B(被阻塞):
  INSERT INTO users(id, name) VALUES(4, 'New');
  -- 被间隙锁阻塞,等待事务 A 释放

2. 增加死锁风险

间隙锁增加锁范围,也增加了死锁概率:

事务 A:SELECT * FROM users WHERE id = 4 FOR UPDATE;
        -- 锁间隙 (3, 5)

事务 B:SELECT * FROM users WHERE id = 6 FOR UPDATE;
        -- 锁间隙 (5, +∞)

事务 A:INSERT INTO users(id) VALUES(6);
        -- 等待事务 B 释放间隙锁

事务 B:INSERT INTO users(id) VALUES(4);
        -- 等待事务 A 释放间隙锁
        -- 死锁!

3. 降低并发性能

间隙锁本质上是”范围锁”,范围越大,阻塞的事务越多,系统并发能力越低。

如何减少间隙锁的影响

1. 使用 READ COMMITTED 隔离级别

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 间隙锁不再使用(除了外键检查)
-- 但需要配合行格式 binlog_row_image=FULL 以及 binlog_format=ROW

2. 优化查询条件

-- 尽量使用唯一索引或主键的精确匹配
SELECT * FROM users WHERE id = 1 FOR UPDATE;  -- 只加记录锁
-- 避免范围查询或非唯一索引的等值查询

3. 缩小事务范围

BEGIN;
-- 只在真正需要的时候加锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 尽快完成操作
UPDATE users SET balance = balance - 100 WHERE id = 1;
COMMIT;  -- 尽早提交,释放锁

4. 使用索引设计规避

  • 唯一索引的等值查询不会产生间隙锁
  • 如果非唯一列也只需要等值查询,考虑加唯一索引

面试要点

  1. 间隙锁 ≠ 记录锁:记录锁锁行,间隙锁锁间隙
  2. 核心作用:阻止幻读(防止其他事务插入新行)
  3. 仅在 REPEATABLE READ 和 SERIALIZABLE 下生效
  4. 间隙锁之间不冲突,只和 INSERT 冲突
  5. 间隙锁是一把双刃剑——解决幻读的同时增加死锁概率、降低并发
  6. READ COMMITTED 下没有间隙锁,这也是很多系统选择 RC 的原因之一

临键锁(Next-Key Lock)详解

临键锁(Next-Key Lock)详解

概述

临键锁(Next-Key Lock)是 InnoDB 在 REPEATABLE READ 隔离级别下默认使用的一种行锁。它本质上是 记录锁(Record Lock)与间隙锁(Gap Lock)的组合,即:锁住一条索引记录的同时,锁住该记录之前的间隙。

临键锁是 InnoDB 实现 REPEATABLE READ 级别下防止幻读的关键机制。

临键锁的结构

临键锁的作用范围可以表示为:(前一个索引值, 当前索引值]

假设索引值为 [1, 3, 5],临键锁的范围是:

(-∞, 1]  — 负无穷到 1 的临键锁
(1, 3]   — 1 到 3 的临键锁
(3, 5]   — 3 到 5 的临键锁
(5, +∞)  — 5 到正无穷的临键锁(此时退化为间隙锁,因为没有上界记录)

公式理解

Next-Key Lock = Gap Lock (前间隙) + Record Lock (当前记录)
     (1, 3]  =    (1, 3)         +        3

临键锁的执行场景

场景一:非唯一索引等值查询

-- age 是普通索引,包含值 [10, 20, 30, 40]

BEGIN;
SELECT * FROM users WHERE age = 20 FOR UPDATE;

加锁范围:
– 在 age 索引上:(10, 20] — 临键锁
– 同时还要锁下一间隙:(20, 30) — 间隙锁(防止幻读,阻止插入 age=20 的重复行)
– 在对应的聚簇索引上,对主键记录加记录锁

为什么还会锁 (20, 30)?因为 age 是非唯一索引,可能有多个 age=20 的行。为了避免其他事务插入新的 age=20 的行,必须锁住下一个间隙。

场景二:范围查询

SELECT * FROM users WHERE age > 20 AND age < 30 FOR UPDATE;

加锁范围:
– 扫描到的所有索引记录,每条记录加上临键锁
– 假设 age 索引有 [10, 20, 30, 40],命中 age=30
– 锁住 (20, 30] + (30, 40) 的间隙锁
– 实际锁范围是 (20, 40),阻止了所有在范围内插入的可能性

场景三:唯一索引查询

-- id 是主键(唯一索引)
SELECT * FROM users WHERE id = 5 FOR UPDATE;

如果 id=5 存在:
– 只加记录锁,不加间隙锁
– 因为唯一索引能精确定位,不需要担心幻读

如果 id=5 不存在:
– 加间隙锁(锁住相邻间隙中的范围)
– 防止其他事务插入 id=5

临键锁的降级

降级为记录锁

当使用唯一索引精确匹配一条存在的记录时,临键锁降级为记录锁:

-- id 为主键,WHERE 条件精确匹配存在的行
SELECT * FROM users WHERE id = 10 FOR UPDATE;
-- 只需记录锁,不需要间隙锁
-- 因为其他事务无法插入 id=10(唯一约束冲突)

降级为间隙锁

当范围查询到达索引边界时,临键锁的”记录”部分可能不存在,退化为纯间隙锁:

-- 已存在索引值 [1, 5, 10]
SELECT * FROM users WHERE id > 10 FOR UPDATE;
-- 没有 id>10 的记录了
-- 只锁间隙 (10, +∞)
-- 这是纯间隙锁,没有记录锁部分

完整示例

假设 orders 表,amount 列上有普通索引,现有值 [100, 200, 300]:

BEGIN;

-- 查询 amount > 150 的记录
SELECT * FROM orders WHERE amount > 150 FOR UPDATE;

加锁分析:

amount 索引扫描范围:200, 300

对于 amount=200:
  临键锁:(100, 200]
  加上下一间隙锁:(200, 300)

对于 amount=300:
  临键锁:(200, 300]
  加上下一间隙锁:(300, +∞)

总锁范围:(100, +∞)

结果:

-- 以下 INSERT 都会被阻塞
INSERT INTO orders(amount) VALUES(150);  -- 被锁在 (100, 200)
INSERT INTO orders(amount) VALUES(250);  -- 被锁在 (200, 300)
INSERT INTO orders(amount) VALUES(350);  -- 被锁在 (300, +∞)

临键锁与幻读的关系

临键锁的本质作用是:锁住”可以插入幻影行”的所有区间。

REPEATABLE READ 下,当前读扫描索引时:
1. 每一条扫描到的记录都被加上临键锁
2. 这意味着这些记录之间的所有间隙都被锁住了
3. 其他事务无法在任何扫描过的间隙中插入新记录
4. 因此下次同样的查询不会多出"幻影"记录

临键锁的缺点

  1. 范围过大:范围查询可能导致大量的间隙被锁定
  2. 死锁风险:多个事务的间隙锁交叉可能引发死锁
  3. 并发下降:锁的范围越大,阻塞的事务越多
  4. 难以预测:锁的范围受实际数据分布影响

如何查看临键锁

-- MySQL 8.0
SELECT * FROM performance_schema.data_locks
WHERE ENGINE_TRANSACTION_ID = 你的事务ID;

-- LOCK_MODE 显示为 X 或 S,结合 LOCK_DATA 和 LOCK_TYPE 判断
-- LOCK_TYPE = RECORD
-- LOCK_MODE = X 表示临键锁(Record Lock + Gap Lock)
-- LOCK_MODE = X,REC_NOT_GAP 表示纯记录锁
-- LOCK_MODE = X,GAP 表示纯间隙锁

面试要点

  1. Next-Key Lock = Record Lock + Gap Lock,范围是(前值, 当前值]
  2. 核心作用:在 REPEATABLE READ 下防止幻读
  3. 唯一索引+已存在行:降级为记录锁
  4. 非唯一索引:临键锁 + 额外间隙锁
  5. 到达索引边界:退化为间隙锁
  6. 如何查看data_locks 表的 LOCK_MODE 区分锁类型

记录锁(Record Lock)详解

记录锁(Record Lock)详解

概述

记录锁(Record Lock)是 InnoDB 行锁中最基础的锁类型。它的作用是锁定索引记录(index record)本身,阻止其他事务对该行记录进行修改或加冲突的锁。理解记录锁,是掌握 InnoDB 锁机制的起点。

基本概念

定义

记录锁是 InnoDB 在索引记录上加的锁。注意不是”行”上,而是”索引记录”上。这一点很重要——InnoDB 的锁都是通过索引实现的。

加锁方式

-- 显式加记录锁(当前读)
SELECT * FROM user WHERE id = 1 FOR UPDATE;            -- 排他锁(X 锁)
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;    -- 共享锁(S 锁)

-- MySQL 8.0 新语法
SELECT * FROM user WHERE id = 1 FOR SHARE;             -- 共享锁

锁的类型

记录锁有两种类型:

锁类型 语法 兼容性
共享锁(S 锁) LOCK IN SHARE MODE / FOR SHARE 多个 S 锁兼容,S 锁与 X 锁互斥
排他锁(X 锁) FOR UPDATE 与所有锁互斥

锁的兼容性矩阵

         S 锁     X 锁
S 锁     ✅      ❌
X 锁     ❌      ❌
  • S 锁之间兼容:两个事务可以同时读取同一行
  • S 锁和 X 锁互斥:读时不能写,写时不能读
  • X 锁之间互斥:同一时间只有一个事务能修改

记录锁的实现细节

必须加在索引上

InnoDB 的行锁是通过索引实现的:

-- 表结构:user(id INT PRIMARY KEY, name VARCHAR(20), age INT, INDEX idx_age(age))

-- 场景 1:WHERE 条件命中主键
SELECT * FROM user WHERE id = 1 FOR UPDATE;
-- 锁:在主键索引(聚簇索引)的 id=1 记录上加 X 锁

-- 场景 2:WHERE 条件命中二级索引
SELECT * FROM user WHERE age = 25 FOR UPDATE;
-- 锁:在二级索引 idx_age 的 age=25 记录上加 X 锁
--     然后通过回表,在聚簇索引的对应记录上也加上 X 锁

-- 场景 3:WHERE 条件无索引
SELECT * FROM user WHERE name = 'Alice' FOR UPDATE;
-- 锁:没有索引可用,InnoDB 会锁全表(走聚簇索引全扫描)
-- 这不是表锁,而是对聚簇索引的每一条记录都加锁

无索引时的极端情况

-- 表:user(id INT PRIMARY KEY, name VARCHAR(20))
-- name 列无索引

-- 事务 A
BEGIN;
SELECT * FROM user WHERE name = 'Alice' FOR UPDATE;

-- 事务 B
UPDATE user SET name = 'Bob' WHERE id = 5;
-- ❌ 被阻塞!虽然事务 A 想锁的是 name='Alice' 的行
-- 但由于无索引,事务 A 实际上锁了所有行

不同隔离级别下的记录锁

READ COMMITTED

在当前读时仅使用 Record Lock,不加间隙锁:

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

BEGIN;
SELECT * FROM user WHERE age = 25 FOR UPDATE;
-- 只锁 age=25 的记录行
-- age=25 前后的间隙不锁定
-- 其他事务可以插入 age=24 或 age=26 的记录

REPEATABLE READ

在当前读时使用 Next-Key Lock(Record Lock + Gap Lock):

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

BEGIN;
SELECT * FROM user WHERE age = 25 FOR UPDATE;
-- 在 REPEATABLE READ 下,即使是点查(不存在的记录)
-- 也会加间隙锁,防止幻读

记录锁的生命周期

事务 BEGIN
    
    
执行当前读语句
    
    
InnoDB 查找索引记录
    
    ├── 找到记录
       └── 在该索引记录上加 Record Lock
           ├── 锁被占用  阻塞等待(innodb_lock_wait_timeout
           └── 锁为空闲  获得锁,执行操作
    
    └── 未找到记录
        └── 在前后间隙加 Gap LockRR 级别下)
    
    
事务 COMMIT / ROLLBACK
    
    
释放所有锁

常见面试场景

场景 1:记录锁解决丢失更新

-- 账务操作:扣减余额
-- ❌ 存在问题:丢失更新
SELECT balance FROM account WHERE id = 1;   -- → 100
-- 此时其他事务也可能读到 100 并进行操作
UPDATE account SET balance = balance - 50 WHERE id = 1;
-- 如果两个事务同时读取到 100,都执行 -50
-- 结果可能是 50(丢失了一次 -50 的操作)

-- ✅ 正确做法:使用记录锁
SELECT balance FROM account WHERE id = 1 FOR UPDATE;  -- 加锁
-- 其他事务无法修改 id=1
UPDATE account SET balance = balance - 50 WHERE id = 1;

场景 2:死锁风险

-- 事务 A                    事务 B
BEGIN;                       BEGIN;
UPDATE user SET ...          UPDATE user SET ...
WHERE id = 1;                WHERE id = 2;
-- 获得 id=1 的 Record Lock   -- 获得 id=2 的 Record Lock
UPDATE user SET ...          UPDATE user SET ...
WHERE id = 2;                WHERE id = 1;
-- 等待 B 释放 id=2           -- 等待 A 释放 id=1
-- ⚠️ 死锁!

解决方法:按固定顺序获取锁(如按 ID 升序)。

面试要点

  1. 本质:Record Lock 是 InnoDB 行锁的基础,锁定的是索引记录
  2. 必须通过索引:没有索引时退化为全表扫描加锁
  3. 两种类型:S 锁(共享)和 X 锁(排他),S 锁可共存,X 锁互斥
  4. 隔离级别影响:RC 下只有 Record Lock,RR 下升级为 Next-Key Lock
  5. 与间隙锁的关系:Record Lock + Gap Lock = Next-Key Lock

间隙锁(Gap Lock)的工作机制

间隙锁(Gap Lock)的工作机制

概述

间隙锁(Gap Lock)是 InnoDB 在 REPEATABLE READ 级别下为了解决幻读问题而引入的一种特殊锁。与记录锁锁定具体行不同,间隙锁锁定的是索引记录之间的间隙(gap),阻止其他事务在这些间隙中插入新记录。

间隙锁的本质

它锁的是什么?

间隙锁锁定的是索引记录之间的间隔(间隙),而不是具体的记录本身。

假设表中有以下数据和索引:

索引值:   1    3    5    7    9
对应的间隙:|---|------|------|------|----|
            (1,3) (3,5) (5,7) (7,9) (9,+∞)

间隙锁可以锁定:
– 两条索引记录之间的间隙
– 第一条索引记录之前的间隙
– 最后一条索引记录之后的间隙

间隙锁的特点

  1. 不锁定记录本身:间隙锁只阻止插入,不阻止对已有记录的修改
  2. 锁的是间隙范围:阻止其他事务在该间隙中插入任何行
  3. 间隙锁可以共存:多个事务可以对同一个间隙加 S 或 X 间隙锁,互不冲突。间隙锁的唯一作用是阻止插入

间隙锁的触发条件

1. 范围查询中使用当前读

-- 假设表中已有 id=1,3,5 的记录
BEGIN;
SELECT * FROM users WHERE id > 3 FOR UPDATE;
-- 锁定的间隙:
-- (3, 5]:Next-Key Lock
-- (5, +∞):间隙锁
-- 其他事务无法插入 id=4 或 id=6 的行

2. 非唯一索引的等值查询

-- age 是非唯一索引
SELECT * FROM users WHERE age = 25 FOR UPDATE;
-- 锁定 age=25 的索引记录以及左右相邻的间隙
-- 防止其他事务插入 age=25 的新行

3. 记录不存在时的锁

SELECT * FROM users WHERE id = 4 FOR UPDATE;
-- 如果 id=4 不存在
-- 锁定 (3, 5) 这个间隙(3 和 5 是相邻的已存在记录)

间隙锁的加锁范围

等值查询

-- 表:id(主键) 有 1, 3, 5

-- 锁定存在的行
SELECT * FROM users WHERE id = 3 FOR UPDATE;
-- 只锁 id=3 的记录(记录锁),不加间隙锁

-- 锁定不存在的行
SELECT * FROM users WHERE id = 4 FOR UPDATE;
-- 锁定间隙 (3, 5)

范围查询

-- 表:id 有 1, 3, 5

SELECT * FROM users WHERE id > 3 AND id < 7 FOR UPDATE;
-- 锁定的范围:
-- (3, 5]:Next-Key Lock(间隙锁 + 记录锁)
-- (5, 7):间隙锁(id=7 不存在,直接锁到上界)

非唯一索引

非唯一索引的等值查询也会加间隙锁:

-- age 是非唯一索引,有 20, 25, 30

SELECT * FROM users WHERE age = 25 FOR UPDATE;
-- 锁定的范围:
-- (20, 25]:Next-Key Lock
-- (25, 30):间隙锁
-- 这是为了防止幻读——阻止其他事务插入 age=25 的新行

间隙锁在不同隔离级别下的行为

REPEATABLE READ(默认)

-- 间隙锁生效
SELECT * FROM orders WHERE amount > 100 FOR UPDATE;
-- 会锁住 amount 索引上的记录和间隙

READ COMMITTED

-- 间隙锁不生效(除了外键约束和唯一键检查时)
SELECT * FROM orders WHERE amount > 100 FOR UPDATE;
-- 只锁匹配的行记录,不锁间隙

间隙锁带来的问题

1. 阻塞插入

事务 A:
  SELECT * FROM users WHERE id > 3 FOR UPDATE;
  -- 锁住 (3, +∞)

事务 B(被阻塞):
  INSERT INTO users(id, name) VALUES(4, 'New');
  -- 被间隙锁阻塞,等待事务 A 释放

2. 增加死锁风险

间隙锁增加锁范围,也增加了死锁概率:

事务 A:SELECT * FROM users WHERE id = 4 FOR UPDATE;
        -- 锁间隙 (3, 5)

事务 B:SELECT * FROM users WHERE id = 6 FOR UPDATE;
        -- 锁间隙 (5, +∞)

事务 A:INSERT INTO users(id) VALUES(6);
        -- 等待事务 B 释放间隙锁

事务 B:INSERT INTO users(id) VALUES(4);
        -- 等待事务 A 释放间隙锁
        -- 死锁!

3. 降低并发性能

间隙锁本质上是”范围锁”,范围越大,阻塞的事务越多,系统并发能力越低。

如何减少间隙锁的影响

1. 使用 READ COMMITTED 隔离级别

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 间隙锁不再使用(除了外键检查)
-- 但需要配合行格式 binlog_row_image=FULL 以及 binlog_format=ROW

2. 优化查询条件

-- 尽量使用唯一索引或主键的精确匹配
SELECT * FROM users WHERE id = 1 FOR UPDATE;  -- 只加记录锁
-- 避免范围查询或非唯一索引的等值查询

3. 缩小事务范围

BEGIN;
-- 只在真正需要的时候加锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 尽快完成操作
UPDATE users SET balance = balance - 100 WHERE id = 1;
COMMIT;  -- 尽早提交,释放锁

4. 使用索引设计规避

  • 唯一索引的等值查询不会产生间隙锁
  • 如果非唯一列也只需要等值查询,考虑加唯一索引

面试要点

  1. 间隙锁 ≠ 记录锁:记录锁锁行,间隙锁锁间隙
  2. 核心作用:阻止幻读(防止其他事务插入新行)
  3. 仅在 REPEATABLE READ 和 SERIALIZABLE 下生效
  4. 间隙锁之间不冲突,只和 INSERT 冲突
  5. 间隙锁是一把双刃剑——解决幻读的同时增加死锁概率、降低并发
  6. READ COMMITTED 下没有间隙锁,这也是很多系统选择 RC 的原因之一

临键锁(Next-Key Lock)详解

临键锁(Next-Key Lock)详解

概述

临键锁(Next-Key Lock)是 InnoDB 在 REPEATABLE READ 隔离级别下默认使用的一种行锁。它本质上是 记录锁(Record Lock)与间隙锁(Gap Lock)的组合,即:锁住一条索引记录的同时,锁住该记录之前的间隙。

临键锁是 InnoDB 实现 REPEATABLE READ 级别下防止幻读的关键机制。

临键锁的结构

临键锁的作用范围可以表示为:(前一个索引值, 当前索引值]

假设索引值为 [1, 3, 5],临键锁的范围是:

(-∞, 1]  — 负无穷到 1 的临键锁
(1, 3]   — 1 到 3 的临键锁
(3, 5]   — 3 到 5 的临键锁
(5, +∞)  — 5 到正无穷的临键锁(此时退化为间隙锁,因为没有上界记录)

公式理解

Next-Key Lock = Gap Lock (前间隙) + Record Lock (当前记录)
     (1, 3]  =    (1, 3)         +        3

临键锁的执行场景

场景一:非唯一索引等值查询

-- age 是普通索引,包含值 [10, 20, 30, 40]

BEGIN;
SELECT * FROM users WHERE age = 20 FOR UPDATE;

加锁范围:
– 在 age 索引上:(10, 20] — 临键锁
– 同时还要锁下一间隙:(20, 30) — 间隙锁(防止幻读,阻止插入 age=20 的重复行)
– 在对应的聚簇索引上,对主键记录加记录锁

为什么还会锁 (20, 30)?因为 age 是非唯一索引,可能有多个 age=20 的行。为了避免其他事务插入新的 age=20 的行,必须锁住下一个间隙。

场景二:范围查询

SELECT * FROM users WHERE age > 20 AND age < 30 FOR UPDATE;

加锁范围:
– 扫描到的所有索引记录,每条记录加上临键锁
– 假设 age 索引有 [10, 20, 30, 40],命中 age=30
– 锁住 (20, 30] + (30, 40) 的间隙锁
– 实际锁范围是 (20, 40),阻止了所有在范围内插入的可能性

场景三:唯一索引查询

-- id 是主键(唯一索引)
SELECT * FROM users WHERE id = 5 FOR UPDATE;

如果 id=5 存在:
– 只加记录锁,不加间隙锁
– 因为唯一索引能精确定位,不需要担心幻读

如果 id=5 不存在:
– 加间隙锁(锁住相邻间隙中的范围)
– 防止其他事务插入 id=5

临键锁的降级

降级为记录锁

当使用唯一索引精确匹配一条存在的记录时,临键锁降级为记录锁:

-- id 为主键,WHERE 条件精确匹配存在的行
SELECT * FROM users WHERE id = 10 FOR UPDATE;
-- 只需记录锁,不需要间隙锁
-- 因为其他事务无法插入 id=10(唯一约束冲突)

降级为间隙锁

当范围查询到达索引边界时,临键锁的”记录”部分可能不存在,退化为纯间隙锁:

-- 已存在索引值 [1, 5, 10]
SELECT * FROM users WHERE id > 10 FOR UPDATE;
-- 没有 id>10 的记录了
-- 只锁间隙 (10, +∞)
-- 这是纯间隙锁,没有记录锁部分

完整示例

假设 orders 表,amount 列上有普通索引,现有值 [100, 200, 300]:

BEGIN;

-- 查询 amount > 150 的记录
SELECT * FROM orders WHERE amount > 150 FOR UPDATE;

加锁分析:

amount 索引扫描范围:200, 300

对于 amount=200:
  临键锁:(100, 200]
  加上下一间隙锁:(200, 300)

对于 amount=300:
  临键锁:(200, 300]
  加上下一间隙锁:(300, +∞)

总锁范围:(100, +∞)

结果:

-- 以下 INSERT 都会被阻塞
INSERT INTO orders(amount) VALUES(150);  -- 被锁在 (100, 200)
INSERT INTO orders(amount) VALUES(250);  -- 被锁在 (200, 300)
INSERT INTO orders(amount) VALUES(350);  -- 被锁在 (300, +∞)

临键锁与幻读的关系

临键锁的本质作用是:锁住”可以插入幻影行”的所有区间。

REPEATABLE READ 下,当前读扫描索引时:
1. 每一条扫描到的记录都被加上临键锁
2. 这意味着这些记录之间的所有间隙都被锁住了
3. 其他事务无法在任何扫描过的间隙中插入新记录
4. 因此下次同样的查询不会多出"幻影"记录

临键锁的缺点

  1. 范围过大:范围查询可能导致大量的间隙被锁定
  2. 死锁风险:多个事务的间隙锁交叉可能引发死锁
  3. 并发下降:锁的范围越大,阻塞的事务越多
  4. 难以预测:锁的范围受实际数据分布影响

如何查看临键锁

-- MySQL 8.0
SELECT * FROM performance_schema.data_locks
WHERE ENGINE_TRANSACTION_ID = 你的事务ID;

-- LOCK_MODE 显示为 X 或 S,结合 LOCK_DATA 和 LOCK_TYPE 判断
-- LOCK_TYPE = RECORD
-- LOCK_MODE = X 表示临键锁(Record Lock + Gap Lock)
-- LOCK_MODE = X,REC_NOT_GAP 表示纯记录锁
-- LOCK_MODE = X,GAP 表示纯间隙锁

面试要点

  1. Next-Key Lock = Record Lock + Gap Lock,范围是(前值, 当前值]
  2. 核心作用:在 REPEATABLE READ 下防止幻读
  3. 唯一索引+已存在行:降级为记录锁
  4. 非唯一索引:临键锁 + 额外间隙锁
  5. 到达索引边界:退化为间隙锁
  6. 如何查看data_locks 表的 LOCK_MODE 区分锁类型

Redlock 争议和局限——分布式系统界最著名的争论之一

Redlock 争议和局限——分布式系统界最著名的争论之一

争议的起源

2016 年,Redis 作者 Antirez 提出了 Redlock 算法。随后分布式系统领域的大神 Martin Kleppmann(《数据密集型应用系统设计》作者)写了一篇著名的文章《How to do distributed locking》,激烈批评 Redlock 算法。

这场争论成为了分布式系统界最引人注目的技术辩论之一。

Martin 的核心批评

批评一:GC pause 破坏互斥性

Martin 认为,即使 Redlock 在技术上是正确的,现实中分布式系统的各种不可控事件仍然会破坏锁的互斥性:

时间线:
1. 客户端 C1 获得 Redlock 锁(超过半数节点写入成功)
2. C1 发生 Full GC(Stop-The-World),持续 10 秒
3. 锁的 TTL 到期(假设 TTL = 5 秒)
4. 其他客户端 C2 获得同一把锁
5. C1 GC 完成,继续执行业务逻辑——认为还持有锁
6. C1 和 C2 同时操作共享资源 → 互斥性被破坏

Martin 的观点:这不是 Redlock 独有的问题,任何基于超时机制的分布式锁都有这个问题。但 Redlock 声称能解决,实际上并 没有根本上解决

批评二:依赖时钟同步

Redlock 假设各节点时钟是一致的。但现实中的时钟问题:

  • NTP 时钟同步有误差
  • 时间回拨(系统管理员调时间、闰秒)
  • 虚拟化平台的时钟抖动

Martin 的模拟攻击:如果某个 Redis 节点的时间发生了跳跃,它可能提前释放了锁,导致客户端提前认为锁已失效或误认为锁还存在。

批评三:Redlock 没有区别于单节点方案的更优理由

Martin 认为,既然 Redlock 也依赖超时 + 续约机制,那为什么要用复杂的多节点方案?

他的建议:不如使用一个简单的 SET NX EX + 操作资源时的 Fencing Token(隔离令牌) 机制:

// 使用 Fencing Token 保护资源
尝试获取锁锁值包含递增的 token 编号
操作资源时带上 token
存储端如数据库验证 token 是否有效
旧的 token 不能操作资源

Antirez 的反驳

Antirez 针对 Martin 的批评一一回应:

关于 GC pause

  • 如果系统会发生长时间的 Stop-The-World,任何分布式锁方案都会受影响
  • Redlock 的 TTL 应该设得足够长,覆盖 GC pause 的可能时长
  • 如果真的出现长达几分钟的 STW,系统本身已经出了问题

关于时钟

  • 生产系统中,服务器使用 NTP 且做了精细校准,时钟跳变的概率极低
  • 如果管理员手动调整时间导致时钟跳变,属于运维事故,不是算法的问题
  • 可以在算法中增加时钟偏移检测,发现异常时主动拒绝

关于 Fencing Token

  • Redis 本身不支持 Fencing Token 的单调递增生成(Redis 的 INCR 在高可用场景下也有同样问题)
  • 如果真的需要 Fencing Token,需要一个能生成严格递增 token 的组件(如 ZooKeeper 的 zxid)

中立派的观点

许多分布式系统工程师最终的观点是:

“Redlock 不是一个糟糕的方案,但它也不是银弹。”

场景 评价
99.99% 的互联网场景 REDlock 足够好了
金融交易、资金操作 建议用 ZooKeeper/Etcd
已有 Redis 集群 用 Redlock 成本最低
资源能在存储层做幂等 完全不需要 Redlock

Redlock 的实际局限

除了理论争议,实际使用中也有一些实际问题:

局限 表现
性能开销 和 5 个节点交互,网络延迟放大
实现复杂 需要处理部分节点失败、超时、重试
时钟敏感 节点间时钟偏差影响有效性
成本 需要 5 台 Redis,比主从成本高
运维复杂 5 个独立 Redis 的部署和监控

面试要点

  • 这场 两位大神的论战 是分布式系统领域的经典讨论
  • 核心争议是 GC pause 和时钟同步 —— 这两个不是 Redlock 独有的问题
  • 面试时能说出双方观点 + 自己的理解是加分项
  • 最终结论:Redlock 有理论缺陷但在实践中足够好
  • 如果你的场景要求绝对正确(金融交易),建议用 ZooKeeper/Etcd + Fencing Token
  • 加分小贴士:提到 Martin Kleppmann 的《DDIA》和 Antirez 的 Redlock 规格说明原文

Redlock 算法原理——Redis 官方分布式锁方案

Redlock 算法原理——Redis 官方分布式锁方案

Redlock 的背景

SET NX EX 方案在单机 Redis 下运行良好,但在主从架构中存在一个致命缺陷:

  1. 客户端 A 向 Master 写入锁
  2. Master 在将锁同步到 Slave 之前宕机
  3. Slave 提升为新的 Master
  4. 客户端 B 向新的 Master 获取同一把锁——成功了!
  5. 这时客户端 A 和 B 都认为自己持有锁,互斥性被打破

为了解决这个问题,Redis 的作者 Antirez 提出了 Redlock(Red Lock)算法

Redlock 的核心思想

“多数派机制”:不依赖单个 Redis 节点,而是同时在多个独立的 Redis 节点上加锁,只要超过半数(N/2 + 1)节点加锁成功,就视为获取锁成功。

算法前提

需要部署 N 个完全独立的 Redis 节点(建议 5 个),这些节点之间没有主从关系,彼此独立:

Redis-1 (独立)
Redis-2 (独立)
Redis-3 (独立)
Redis-4 (独立)
Redis-5 (独立)

不复制、不协调、互相不知道彼此的存在。

加锁过程

步骤一:获取当前时间

客户端记录当前时间戳 T1

步骤二:逐一向各节点加锁

客户端依次向 N 个 Redis 节点发送 SET NX EX 请求,每一个节点使用相同的 key 和 相同的随机 value

关键参数
– 每个节点的加锁超时时间(timeout)应远小于锁的总有效时间(TTL)
– 例如:锁 TTL = 10 秒,单节点加锁超时 = 5~50ms
– 目的是尽快失败,不等待挂掉的节点

步骤三:计算总耗时

获取完成锁后的时间 T2,计算总耗时:

elapsed = T2 - T1

步骤四:判断是否加锁成功

如果同时满足以下两个条件,则加锁成功:

  1. 超过半数节点(N/2 + 1)加锁成功
  2. 加锁总耗时小于锁的有效期(TTL – elapsed > 0)

步骤五:计算锁有效时间

锁真正的有效时间:

valid_time = TTL - elapsed

客户端必须在 valid_time 内完成业务,超过这个时间锁可能已经失效。

释放锁

向所有 Redis 节点发送释放锁的 Lua 脚本,不管加锁时是否成功

// 向所有5个节点发送释放锁脚本
for (RedisClient client : allNodes) {
    client.eval(luaScript, keys, args);
}

这样即使某个节点因为网络问题加锁失败,释放时也会清理,不会有残留。

算法流程图

开始加锁
  ↓
获取本地时间 T1
  ↓
┌──────────────────────────────────────────┐
│ for each Redis 节点:                     │
│   尝试 SET key value NX EX TTL           │
│   设置单节点超时 10ms                    │
└──────────────────────────────────────────┘
  ↓
获取本地时间 T2, 计算总耗时 elapsed = T2 - T1
  ↓
成功节点数 >= 3  AND elapsed < TTL ?
  ├──   加锁成功, 锁有效期 = TTL - elapsed
  └──   加锁失败, 向所有节点发送释放指令

价值与风险

Redlock 解决的根本问题

SET NX EX 方案在主从模式下丢失锁的问题——即使主节点宕机,只要有足够多的独立节点存活,其他节点上的锁不受影响。

Redlock 没有解决的问题

  1. 性能下降:需要和 5 个节点交互,延迟增加
  2. 时钟依赖:假设各节点时钟一致,如果节点间时钟偏差大,算法可能失效
  3. GC pause 问题:客户端 Full GC 期间锁过期,但业务继续执行

生产环境怎么选

场景 推荐方案 理由
非关键业务锁 SET NX EX + 主从 简单可靠
关键互斥锁,允许少量不一致 Redlock 官方推荐
对一致性要求极高 ZooKeeper/Etcd 强一致性方案
已有 ZooKeeper/Etcd 基础 用已有的 减少维护成本

面试要点

  • Redlock 的本质是 多数派生效机制(类似 Raft/Paxos 的 quorum 思想)
  • 需要 N=5 个独立节点,需要半数以上(≥3)加锁成功
  • 计算有效时间时要减去加锁操作的耗时
  • 释放时要向所有节点发送释放请求(包括加锁失败的)
  • 核心缺陷:依赖时钟、GC pause、性能开销——这些引出了 Redlock 争议

threading.Thread 创建线程的两种方式

threading.Thread 创建线程的两种方式

创建线程的多种方式

Python 的 threading 模块提供了多种创建和管理线程的方式。最常用的是 Thread 类。

方式一:直接使用 Thread

import threading
import time

def worker(name, delay):
    """线程工作函数"""
    print(f"线程 {name} 启动")
    time.sleep(delay)
    print(f"线程 {name} 完成")

# 创建并启动线程
t1 = threading.Thread(target=worker, args=("A", 2))
t2 = threading.Thread(target=worker, args=("B", 1))

t1.start()
t2.start()

# 等待线程结束
t1.join()
t2.join()

print("所有线程已完成")

方式二:继承 Thread 类

import threading
import time

class DownloadThread(threading.Thread):
    def __init__(self, url, output):
        super().__init__()
        self.url = url
        self.output = output
        self.result = None

    def run(self):
        """线程启动时自动调用 run()"""
        print(f"开始下载: {self.url}")
        time.sleep(2)  # 模拟下载
        self.result = f"内容来自 {self.url}"
        print(f"下载完成: {self.url}")

threads = [
    DownloadThread("https://a.com", "a.txt"),
    DownloadThread("https://b.com", "b.txt"),
]

for t in threads:
    t.start()

for t in threads:
    t.join()
    print(f"结果: {t.result}")  # 获取线程结果

方式三:使用线程池

from concurrent.futures import ThreadPoolExecutor
import time

def task(n):
    time.sleep(1)
    return n ** 2

# 自动管理线程生命周期
with ThreadPoolExecutor(max_workers=4) as executor:
    # 方式 A:逐个提交
    future = executor.submit(task, 5)
    print(future.result())  # 25

    # 方式 B:批量映射
    results = list(executor.map(task, range(10)))
    print(results)  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

线程生命周期

stateDiagram-v2
    [*] --> Created: threading.Thread()
    Created --> Runnable: start()
    Runnable --> Running: 获取 CPU(获取 GIL
    Running --> Runnable: GIL 释放/切换
    Runnable --> Blocked: I/O 等待/
    Blocked --> Runnable: I/O 完成/锁释放
    Running --> Dead: run() 完成/异常
    Dead --> [*]

线程参数详解

import threading

def advanced_thread():
    # 命名线程(方便调试)
    t = threading.Thread(
        target=worker,
        name="Worker-1",      # 线程名称
        args=("A",),           # 位置参数
        kwargs={"delay": 0},   # 关键字参数
        daemon=True,           # 是否为守护线程
    )
    print(t.name)       # Worker-1
    print(t.daemon)     # True
    print(t.ident)      # 线程 ID(启动后才有)
    t.start()

获取线程信息

import threading
import time

def show_thread_info():
    # 当前线程信息
    current = threading.current_thread()
    print(f"当前线程: {current.name}")
    print(f"线程 ID: {current.ident}")
    print(f"是否守护线程: {current.daemon}")
    print(f"是否活跃: {current.is_alive()}")

    # 活跃线程列表
    print(f"活跃线程数: {threading.active_count()}")
    for t in threading.enumerate():
        print(f"  - {t.name} (daemon={t.daemon})")

# 主线程
show_thread_info()

t = threading.Thread(target=lambda: (time.sleep(1), show_thread_info()))
t.start()
t.join()

线程池的高级用法

from concurrent.futures import ThreadPoolExecutor, as_completed
import time

def fetch_data(user_id):
    """模拟 API 调用"""
    time.sleep(user_id * 0.5)
    return {"user_id": user_id, "name": f"User_{user_id}"}

user_ids = [1, 2, 3, 4, 5]

with ThreadPoolExecutor(max_workers=3) as executor:
    # 提交所有任务
    future_to_id = {
        executor.submit(fetch_data, uid): uid
        for uid in user_ids
    }

    # 按完成顺序处理结果
    for future in as_completed(future_to_id):
        uid = future_to_id[future]
        try:
            data = future.result()
            print(f"用户 {uid} 数据获取成功: {data}")
        except Exception as e:
            print(f"用户 {uid} 获取失败: {e}")

常见陷阱

# 陷阱 1:忘记 join() 导致主线程提前退出
t = threading.Thread(target=worker, args=("X", 2))
t.start()
# 如果主线程在这里结束,线程 X 可能被强制终止
t.join()  # 必须等待!

# 陷阱 2:多次 start()
t = threading.Thread(target=worker, args=("Y", 1))
t.start()
# t.start()  # RuntimeError: threads can only be started once

# 陷阱 3:共享可变对象的传递
shared_list = []

def bad_worker():
    shared_list.append("data")
    # 这里 shared_list 是全局共享的,非线程安全

# 推荐:使用队列或线程安全的数据结构
from queue import Queue
safe_queue = Queue()

def good_worker():
    safe_queue.put("data")

面试高频题

Q: Thread.join(timeout) 的作用是什么?

A: 设置超时时间。如果线程在超时内未完成,join() 返回但不抛出异常。可以通过 t.is_alive() 判断线程是否仍在运行。

Q: 为什么不推荐继承 Thread 而推荐用 target

A: 继承 Thread 破坏了类的单一职责原则(线程管理和业务逻辑耦合),且无法轻松使用线程池。使用 target 更灵活。

Q: 线程的 daemon 参数有什么用?

A: 守护线程在主线程结束时自动退出(不等待其完成)。适合后台监控、心跳检测等非关键任务。


concurrent.futures 线程池与进程池统一接口

concurrent.futures 线程池与进程池统一接口

高级并发接口

concurrent.futures 是 Python 3.2 引入的高级并发模块,提供了统一的线程池和进程池接口。它的核心思想是将任务的提交结果获取解耦。

ThreadPoolExecutor vs ProcessPoolExecutor

from concurrent.futures import (
    ThreadPoolExecutor,
    ProcessPoolExecutor,
    as_completed,
    wait,
    FIRST_COMPLETED,
    ALL_COMPLETED
)
graph TB
    subgraph "concurrent.futures"
        E[Executor
抽象基类
] --> TE[ThreadPoolExecutor] E --> PE[ProcessPoolExecutor] TE --> F1[Future 对象] PE --> F2[Future 对象] F1 --> R1[result 方法] F2 --> R2[result 方法] end

基本用法

from concurrent.futures import ThreadPoolExecutor
import time

def fetch_url(url):
    time.sleep(1)
    return f"{url} 的数据"

# submit —— 逐个提交
with ThreadPoolExecutor(max_workers=3) as executor:
    future = executor.submit(fetch_url, "https://a.com")
    print(future.result())  # 阻塞直到完成

# map —— 批量提交,按输入顺序返回
with ThreadPoolExecutor(max_workers=3) as executor:
    results = executor.map(
        fetch_url,
        ["https://a.com", "https://b.com", "https://c.com"]
    )
    for result in results:
        print(result)  # 顺序与输入一致

Future 对象

Future 代表一个异步操作的结果,提供了查询状态和获取结果的方法:

from concurrent.futures import ThreadPoolExecutor
import time

def slow_task(n):
    time.sleep(n)
    if n > 3:
        raise ValueError(f"n={n} 太大了")
    return n * 2

with ThreadPoolExecutor() as executor:
    future = executor.submit(slow_task, 2)

    # 检查状态
    print(future.done())      # False(可能尚未完成)
    time.sleep(2)
    print(future.done())      # True

    # 获取结果(阻塞)
    print(future.result())    # 4

    # 带超时的结果获取
    # print(future.result(timeout=1.0))  # TimeoutError

    # 取消任务(未启动时有效)
    print(future.cancel())    # True/False
    print(future.cancelled()) # True/False

    # 异常处理
    future2 = executor.submit(slow_task, 5)
    try:
        result = future2.result()
    except ValueError as e:
        print(f"捕获异常: {e}")

as_completed —— 异步迭代

as_completed 按任务完成顺序返回结果:

from concurrent.futures import ThreadPoolExecutor, as_completed
import time
import random

def task(n):
    delay = random.uniform(0.5, 2.0)
    time.sleep(delay)
    return f"任务 {n}(耗时 {delay:.2f}s)"

with ThreadPoolExecutor(max_workers=5) as executor:
    futures = [executor.submit(task, i) for i in range(10)]

    # 按完成顺序处理
    for future in as_completed(futures):
        result = future.result()
        print(result)

wait —— 精细控制

from concurrent.futures import ThreadPoolExecutor, wait, FIRST_COMPLETED
import time

def task_a(): time.sleep(3); return "A"
def task_b(): time.sleep(1); return "B"
def task_c(): time.sleep(2); return "C"

with ThreadPoolExecutor(max_workers=3) as executor:
    futures = [
        executor.submit(task_a),
        executor.submit(task_b),
        executor.submit(task_c),
    ]

    # 等待第一个完成
    done, pending = wait(futures, return_when=FIRST_COMPLETED)
    for f in done:
        print(f"第一个完成: {f.result()}")  # B(1 秒)

    # 等待所有完成
    done, pending = wait(pending, return_when="ALL_COMPLETED")
    print(f"全部完成: {len(done)} 个")

进程池用法

from concurrent.futures import ProcessPoolExecutor
import math

def is_prime(n):
    """检查素数(CPU 密集型)"""
    if n < 2:
        return False
    for i in range(2, int(math.sqrt(n)) + 1):
        if n % i == 0:
            return False
    return True

# 多进程——真正利用多核
with ProcessPoolExecutor(max_workers=4) as executor:
    numbers = range(10_000_000, 10_001_000)
    results = executor.map(is_prime, numbers)
    primes = [n for n, r in zip(numbers, results) if r]
    print(f"找到 {len(primes)} 个素数")

实战:网络请求 + 计算

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time
import math

# 第一阶段:I/O —— 多线程
def fetch_prices(stock_ids):
    with ThreadPoolExecutor(max_workers=10) as io_executor:
        prices = list(io_executor.map(
            lambda sid: {"id": sid, "price": 100 + sid},
            stock_ids
        ))
    return prices

# 第二阶段:CPU —— 多进程
def calculate_risk(prices_data):
    def compute(pd):
        # 假设复杂的风险评估
        time.sleep(0.1)
        return pd["id"] * pd["price"]

    with ProcessPoolExecutor(max_workers=4) as cpu_executor:
        risks = list(cpu_executor.map(compute, prices_data))
    return risks

# 流水线执行
stocks = list(range(100))
prices = fetch_prices(stocks)
risks = calculate_risk(prices)
print(f"完成 {len(risks)} 个风险评估")

异常处理最佳实践

from concurrent.futures import ThreadPoolExecutor, as_completed

def safe_submit(executor, fn, *args, **kwargs):
    """安全提交任务,捕获初始化异常"""
    try:
        return executor.submit(fn, *args, **kwargs)
    except RuntimeError as e:
        print(f"提交失败: {e}")
        return None

with ThreadPoolExecutor(max_workers=5) as executor:
    futures = []

    for item in range(10):
        future = safe_submit(executor, risky_task, item)
        if future:
            futures.append(future)

    # 处理结果(逐个 try-except)
    for future in as_completed(futures):
        try:
            result = future.result(timeout=5)
            print(f"成功: {result}")
        except TimeoutError:
            print("任务超时")
        except Exception as e:
            print(f"任务失败: {e}")

重要参数

参数 说明 默认值
max_workers 最大工作线程/进程数 min(32, cpu_count + 4)
initializer 初始化函数 None
initargs 初始化函数参数 ()
import threading

# 线程本地存储初始化
local_data = threading.local()

def init_worker():
    local_data.user_id = os.environ.get("USER_ID", "unknown")
    print(f"Worker initialized: {local_data.user_id}")

with ThreadPoolExecutor(
    max_workers=4,
    initializer=init_worker
) as executor:
    pass

面试高频题

Q: concurrent.futures 和直接使用 threading/multiprocessing 有何优劣?

A: futures 更抽象,适合”提交-等待-获取结果”模型;原生接口更适合需要精细控制(如自定义锁、事件)的场景。futures 代码更简洁、错误处理更统一。

Q: ThreadPoolExecutor.map 和内置 map 有什么区别?

A: ThreadPoolExecutor.map 并发执行,但返回的顺序与输入顺序一致(而非完成顺序)。如果不需要按序处理,用 as_completed 更高效。

Q: max_workers 设置为多少合适?

A: 线程池:I/O 密集型可设置较大(几十到上百);进程池:CPU 密集型通常设为 cpu_count。设置过大反而会因上下文切换降低性能。


线程同步 Lock RLock

线程同步 Lock RLock

为什么需要锁

当多个线程同时访问共享数据时,会发生竞态条件(Race Condition)—— 数据可能被意外破坏。

import threading

counter = 0

def increment():
    global counter
    for _ in range(1_000_000):
        # 这三行实际上不是原子的
        # counter += 1 实际上是:
        temp = counter       # 1. 读取
        temp += 1            # 2. 计算
        counter = temp       # 3. 写入

threads = [threading.Thread(target=increment) for _ in range(5)]
for t in threads: t.start()
for t in threads: t.join()
print(f"期望: 5,000,000, 实际: {counter}")  # 不是 500 万!

Lock(互斥锁)

import threading

counter = 0
lock = threading.Lock()  # 创建锁

def safe_increment():
    global counter
    for _ in range(1_000_000):
        lock.acquire()  # 获取锁
        try:
            counter += 1  # 临界区
        finally:
            lock.release()  # 释放锁

# 更 Pythonic 的写法:使用上下文管理器
def safe_increment_v2():
    global counter
    for _ in range(1_000_000):
        with lock:  # 自动 acquire + release
            counter += 1
sequenceDiagram
    participant T1 as 线程 1
    participant L as Lock
    participant T2 as 线程 2
    participant C as counter

    T1->>L: acquire()
    L-->>T1: 获取成功
    T1->>C: counter += 1
    T1->>L: release()
    T1->>L: acquire()
    L-->>T2: 线程 2 等待中...
    T1->>C: counter += 1
    T1->>L: release()
    L-->>T2: 线程 2 获取成功
    T2->>C: counter += 1
    T2->>L: release()

锁的属性和方法

import threading

lock = threading.Lock()

# 非阻塞尝试获取锁
if lock.acquire(blocking=False):
    # 立即获取锁,不等待
    lock.release()

# 带超时的获取
if lock.acquire(timeout=2.0):
    # 等待最多 2 秒
    lock.release()

# 检查锁状态
print(lock.locked())  # True 或 False

RLock(可重入锁)

RLock 允许同一个线程多次获取锁而不会死锁。每个 acquire() 必须有对应的 release()

import threading

rlock = threading.RLock()

# 同一个线程可以多次 acquire
def recursive_func(n):
    with rlock:
        print(f"深度: {n}")
        if n > 0:
            recursive_func(n - 1)  # 递归获取同一个锁

recursive_func(3)  # 正常运行!

# 对比:如果使用普通 Lock,递归调用会死锁
def recursive_deadlock(n):
    with lock:  # 普通 Lock
        print(f"深度: {n}")
        if n > 0:
            recursive_deadlock(n - 1)  # 死锁!

Lock vs RLock

# Lock 的局限
lock = threading.Lock()
lock.acquire()
lock.acquire(blocking=False)  # 立即返回 False(已持有锁)
lock.release()

# RLock 的计数机制
rlock = threading.RLock()
print(rlock.acquire())  # True, count=1
print(rlock.acquire())  # True, count=2
rlock.release()          # count=1
rlock.release()          # count=0, 锁被真正释放

常见同步情景

1. 有限资源池

class ResourcePool:
    def __init__(self, resources):
        self._resources = list(resources)
        self._lock = threading.Lock()

    def acquire(self):
        with self._lock:
            if not self._resources:
                raise RuntimeError("没有可用资源")
            return self._resources.pop()

    def release(self, resource):
        with self._lock:
            self._resources.append(resource)

pool = ResourcePool(["conn_1", "conn_2", "conn_3"])

def worker(n):
    try:
        conn = pool.acquire()
        print(f"工作 {n} 获取 {conn}")
        time.sleep(1)
        pool.release(conn)
        print(f"工作 {n} 释放 {conn}")
    except RuntimeError as e:
        print(f"工作 {n}: {e}")

threads = [threading.Thread(target=worker, args=(i,)) for i in range(5)]

2. 读写锁(使用 RLock)

class ReadWriteLock:
    """简单读写锁实现"""
    def __init__(self):
        self._read_ready = threading.Lock()
        self._readers = 0
        self._write_lock = threading.Lock()

    def read_acquire(self):
        with self._read_ready:
            self._readers += 1
            if self._readers == 1:
                self._write_lock.acquire()

    def read_release(self):
        with self._read_ready:
            self._readers -= 1
            if self._readers == 0:
                self._write_lock.release()

    def write_acquire(self):
        self._write_lock.acquire()

    def write_release(self):
        self._write_lock.release()

死锁示例

import threading

lock_a = threading.Lock()
lock_b = threading.Lock()

def thread_1():
    with lock_a:
        print("T1: 获取锁 A")
        with lock_b:
            print("T1: 获取锁 B")

def thread_2():
    with lock_b:
        print("T2: 获取锁 B")
        with lock_a:
            print("T2: 获取锁 A")

最佳实践

# ✅ 好:最小化临界区
with lock:
    counter += 1  # 只在需要保护的地方加锁

# ✅ 好:使用 with 语句自动管理
with lock:
    shared_resource.modify()

# ❌ 差:临界区过大
def slow_function():
    with lock:
        result = complex_calculation()
        data = fetch_from_db()
        process_data(data)
        save_to_cache(result)

面试高频题

Q: LockRLock 在性能上有区别吗?

A: RLockLock 稍慢(需要维护持有线程和重入计数)。如果你不需要重入特性,用 Lock 性能略优。

Q: threading.Lockthreading.Semaphore 有什么区别?

A: Lock 是互斥的(一次只有一个线程),Semaphore 允许多个线程同时访问(计数信号量)。Semaphore(1) 等价于 Lock

Q: 锁竞争太激烈怎么办?

A: 策略包括:减小临界区、用读写锁替代互斥锁、用原子操作替代锁(queue.Queuemultiprocessing.Value)、用无锁数据结构。


runtime.LockOSThread() 的使用场景

runtime.LockOSThread() 的使用场景

概述

runtime.LockOSThread() 用于将当前 goroutine 绑定到当前的操作系统线程上。被绑定的 goroutine 只能在该线程上运行,该线程也只会执行这个 goroutine(除非 goroutine 退出或调用 UnlockOSThread)。

func LockOSThread()
func UnlockOSThread()

核心机制

graph TD
    subgraph 正常情况
        G1[Goroutine A] -->|可运行| P1[P] --> M1[OS Thread 1]
        G2[Goroutine B] -->|可运行| P1 --> M1
        G3[Goroutine C] -->|可运行| P1 --> M1
    end

    subgraph LockOSThread 
        G4[Goroutine D - Locked] -->|唯一| P2[P] --> M2[OS Thread 2 - Dedicated]
        G5[Goroutine E] --> P3[P] --> M3[OS Thread 3]
        G6[Goroutine F] --> P3 --> M3
    end

主要使用场景

1. CGo 调用与线程局部状态

这是最常见的使用场景。 许多 C 库要求函数调用必须在同一个操作系统线程中执行,因为它们依赖于线程局部存储(TLS)。

/*
#include 
// 假设 C 库需要线程局部存储
*/
import "C"
import "runtime"

func callCLibrary() {
    runtime.LockOSThread()
    defer runtime.UnlockOSThread()

    // 所有 C 调用都保证在同一个 OS 线程中执行
    C.my_c_func()
}

典型的例子包括:
– OpenGL 图形操作(线程上下文绑定)
– 数据库驱动(如 SQLite 的某些模式)
– 使用线程局部缓存的 C 语言库

2. 实时系统中的 CPU 亲和性

import (
    "runtime"
    "golang.org/x/sys/unix"
)

func realtimeTask() {
    runtime.LockOSThread()
    defer runtime.UnlockOSThread()

    // 设置 CPU 亲和性,将当前线程绑定到特定 CPU 核心
    var mask unix.CPUSet
    mask.Set(0) // CPU 0
    unix.SchedSetaffinity(0, &mask)

    // 设置实时调度策略
    policy := unix.SCHED_FIFO
    params := &unix.SchedParam{Priority: 99}
    unix.SchedSetscheduler(0, policy, params)

    // 执行实时任务
    for {
        processRealtimeData()
    }
}

3. 信号处理

Go 运行时使用独立的线程处理信号。当需要确保信号处理 goroutine 在固定线程上运行时:

func signalHandler(sig chan os.Signal) {
    runtime.LockOSThread()
    defer runtime.UnlockOSThread()

    for s := range sig {
        switch s {
        case syscall.SIGTERM:
            gracefulShutdown()
        }
    }
}

4. Cocoa/UIKit(macOS/iOS)主线程

在 Go 中调用 macOS 或 iOS 的 GUI 框架时,UI 操作必须在主线程上执行:

// #cgo LDFLAGS: -framework Cocoa
import "C"

func runOnMainThread(fn func()) {
    runtime.LockOSThread()
    defer runtime.UnlockOSThread()

    // 运行 Cocoa 事件循环
    C.run_app()
}

注意事项和陷阱

1. goroutine 永远不会迁移

func worker() {
    runtime.LockOSThread()
    defer runtime.UnlockOSThread()

    // 如果这里调用了 runtime.Gosched(),当前 goroutine 仍然和 OS 线程绑定
    // 但真正运行的是下一个 goroutine?
    // 不,LockOSThread 后,OS 线程只运行当前 goroutine
}

2. 死线程风险

// 危险:LockOSThread 后 goroutine 退出时
// 如果忘记调用 UnlockOSThread,OS 线程会泄漏
func dangerous() {
    runtime.LockOSThread()
    // 忘记 defer runtime.UnlockOSThread()
    // goroutine 退出时,这个 OS 线程变为不可用
}

3. 对 Go 调度的影响

LockOSThread 会降低 Go 调度器的灵活性。如果很多 goroutine 都 LockOSThread,Go 运行时会创建更多的 OS 线程,增加系统开销。

4. 与 GOMAXPROCS 的关系

func main() {
    // 假设 GOMAXPROCS=4
    runtime.GOMAXPROCS(4)

    for i := 0; i < 10; i++ {
        go func() {
            runtime.LockOSThread()
            // 10 个 Locked goroutine + 4 个 P
            // 会创建最多 10 个 OS 线程
        }()
    }
}

检测 LockOSThread 的使用

// 判断当前 goroutine 是否绑定了 OS 线程
if runtime.LockOSThread() {
    // 已经绑定
}

// 注意:runtime.LockOSThread 没有返回值
// 可以使用 runtime.CheckLockOSThread 来判断(实际上不存在这个函数)
// 需要通过以下方式:
locked := false
// 通过一些 hack 方式检测

最佳实践

  1. 只在必要时使用:默认情况下不需要 LockOSThread。
  2. 始终配对:LockOSThread 和 UnlockOSThread 必须成对出现,推荐用 defer
  3. 尽早调用:在 goroutine 函数的最开始就调用 LockOSThread。
  4. 避免长时间绑定:完成必要操作后尽快解锁。
  5. CGo 优先:如果 C 库明确要求 TLS,才使用 LockOSThread。

总结

runtime.LockOSThread() 是一个底层控制工具,用于将 goroutine 固定到特定 OS 线程。它的主要使用场景是 CGo 调用、线程局部存储和系统级编程。在普通 Go 应用中很少需要用到它,但理解它的原理有助于深入理解 Go 的调度模型。


线程生命周期详解:Java 线程的6种状态及状态转换

线程生命周期详解:Java 线程的6种状态及状态转换

一、定义

Java 线程在生命周期中有 6 种明确定义的状态,定义在 Thread.State 枚举中。这些状态反映了 JVM 视角下的线程生命周期,与操作系统的线程状态不完全一一对应,但提供了 Java 层面的抽象。

// Thread.State 枚举定义
public enum State {
    NEW,        // 新建
    RUNNABLE,   // 可运行
    BLOCKED,    // 阻塞(等待monitor锁)
    WAITING,    // 等待(无限期)
    TIMED_WAITING, // 限期等待
    TERMINATED  // 终止
}

二、6种状态详解

1. NEW(新建)

线程对象刚创建,尚未调用 start()。此时线程对象已被分配到堆内存,但 JVM 尚未为其分配操作系统线程资源。

2. RUNNABLE(可运行)

调用 start() 后进入此状态。包含两个子状态:
Ready(就绪):等待 CPU 调度
Running(运行中):获得 CPU 时间片正在执行

JVM 层面不区分 Ready 和 Running,统一视为 RUNNABLE。

3. BLOCKED(阻塞)

线程等待获取 监视器锁(monitor lock) 时进入此状态。通常发生在:
– 试图进入 synchronized 代码块/方法但锁被其他线程持有
– 调用 Object.wait() 返回后重新竞争锁

4. WAITING(等待)

线程无限期等待另一个线程执行特定操作。进入方式:
Object.wait()(无超时)
Thread.join()(无超时)
LockSupport.park()

5. TIMED_WAITING(限期等待)

线程在指定时间内等待。进入方式:
Thread.sleep(ms)
Object.wait(timeout)
Thread.join(ms)
LockSupport.parkNanos(nanos)
LockSupport.parkUntil(deadline)

6. TERMINATED(终止)

线程执行完毕或抛出未捕获异常。一旦进入此状态,不能再次调用 start(),否则抛出 IllegalThreadStateException

三、状态转换图

flowchart TD
    NEW -->|"thread.start()"| RUNNABLE
    RUNNABLE -->|"run()结束或异常"| TERMINATED
    RUNNABLE -->|"获取synchronized锁失败"| BLOCKED
    BLOCKED -->|"获得锁"| RUNNABLE
    RUNNABLE -->|"Object.wait()/Thread.join()/LockSupport.park()"| WAITING
    WAITING -->|"notify()/notifyAll()/join的线程结束/unpark()"| RUNNABLE
    RUNNABLE -->|"Thread.sleep()/wait(timeout)/join(ms)/parkNanos()"| TIMED_WAITING
    TIMED_WAITING -->|"超时/被唤醒/join结束/unpark()"| RUNNABLE

四、代码示例

/**
 * 线程6种状态演示
 */
public class ThreadStateDemo {

    public static void main(String[] args) throws InterruptedException {
        // 1. NEW 状态
        Thread t = new Thread(() -> {
            try {
                // 4. TIMED_WAITING 状态
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        System.out.println("NEW: " + t.getState()); // NEW

        // 2. RUNNABLE 状态
        t.start();
        System.out.println("RUNNABLE: " + t.getState()); // RUNNABLE

        // 3. TIMED_WAITING 状态(sleep期间)
        Thread.sleep(100);
        System.out.println("TIMED_WAITING: " + t.getState()); // TIMED_WAITING

        // 等待线程结束
        t.join();
        // 5. TERMINATED 状态
        System.out.println("TERMINATED: " + t.getState()); // TERMINATED
    }
}

BLOCKED 状态演示

/**
 * 演示BLOCKED状态:两个线程竞争同一个synchronized锁
 */
public class BlockedStateDemo {

    private static final Object LOCK = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (LOCK) {
                while (true) { /* 持有锁不释放 */ }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (LOCK) {
                System.out.println("t2 获取到锁");
            }
        }, "t2");

        t1.start();
        Thread.sleep(100); // 确保 t1 先拿到锁
        t2.start();
        Thread.sleep(100);

        System.out.println("t2 状态: " + t2.getState()); // BLOCKED
        System.exit(0);
    }
}

五、状态对比表格

状态 进入条件 退出条件 是否释放已持有锁
NEW new Thread() start()
RUNNABLE start()、被唤醒、获得锁 CPU 时间片用完、进入阻塞/等待/终止
BLOCKED 尝试进入synchronized块但锁被占用 其他线程释放该锁 ❌ 未获取到锁
WAITING Object.wait()无超时 notify()/notifyAll() ✅ 释放锁
WAITING Thread.join()无超时 被join线程结束 ❌ 不释放锁
WAITING LockSupport.park() unpark() ❌ 不释放锁
TIMED_WAITING Thread.sleep(ms) 超时 ❌ 不释放锁
TIMED_WAITING Object.wait(timeout) 超时/被唤醒 ✅ 释放锁
TERMINATED run()结束或异常

六、重要区别

对比项 BLOCKED WAITING TIMED_WAITING
触发原因 被动等待锁(竞争) 主动等待通知 主动等待超时或通知
典型场景 synchronized竞争 Object.wait() Thread.sleep()
唤醒机制 锁被释放后自动竞争 需notify/notifyAll 超时自动唤醒
sleep是否释放锁 不释放

七、面试常见问题

Q1: BLOCKED 和 WAITING 的核心区别是什么?
BLOCKED 是被动等待 monitor 锁,由 synchronized 关键字引发,锁释放后自动竞争;WAITING 是线程主动等待其他线程执行特定操作,只能由 notify()/notifyAll() 显式唤醒。

Q2: Thread.sleep(0) 有意义吗?
有。它让当前线程立即放弃剩余时间片,触发一次 CPU 重新调度。在高并发场景下可用于线程让步,让相同优先级的其他线程获得执行机会。

Q3: 如何检测线程是否存活?
thread.isAlive() 返回 true 表示线程已启动且未终止(即处于 RUNNABLE/BLOCKED/WAITING/TIMED_WAITING 状态之一)。

Q4: 线程 TERMINATED 后可以重新 start() 吗?
不能。IllegalThreadStateException 会被抛出。线程生命周期不可逆,一个线程只能启动一次。

>

创建线程的4种方式详解:Thread、Runnable、Callable、线程池

创建线程的4种方式详解:Thread、Runnable、Callable、线程池

一、定义

Java 中创建线程有 4 种主要方式,适用于不同的场景。从本质上看,所有方式的最终实现都依赖于 new Thread()start() 调用。

二、四种方式详解

方式一:继承 Thread 类

直接继承 Thread 类并重写 run() 方法。

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 执行中");
        // 业务逻辑
    }
}

// 使用
MyThread t = new MyThread();
t.setName("继承Thread-线程");
t.start();

优点: 简单直接,可通过 this 直接引用当前线程
缺点: ① Java 单继承限制,无法继承其他类;② 任务逻辑与线程耦合,不利于复用;③ 线程池无法直接接受 Thread 子类

方式二:实现 Runnable 接口

实现 Runnable 接口并提供 run() 方法实现。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 执行中");
    }
}

// 使用
Thread t = new Thread(new MyRunnable(), "实现Runnable-线程");
t.start();

// Lambda 简化写法
new Thread(() -> System.out.println("Lambda方式"), "lambda-thread").start();

优点: ① 任务与线程分离,可复用于线程池;② 接口方式更灵活(可同时实现其他接口或继承其他类)
缺点: ① 无返回值;② 无法抛受检异常(run() 方法签名限制)

方式三:实现 Callable 接口 + FutureTask

实现 Callable 接口,配合 FutureTask 包装后传给 Thread。

class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        // 可以抛异常,也可以返回值
        Thread.sleep(1000);
        return "任务执行结果:" + System.currentTimeMillis();
    }
}

// 使用
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
Thread t = new Thread(futureTask, "实现Callable-线程");
t.start();

// 获取返回值(会阻塞直到任务完成)
String result = futureTask.get();  // 抛出 InterruptedException, ExecutionException
System.out.println(result);

// 带超时的获取
String resultWithTimeout = futureTask.get(2, TimeUnit.SECONDS);

优点: ① 有返回值(泛型指定);② 可以抛受检异常
缺点: 使用相对复杂(需要 FutureTask 桥接)

方式四:线程池(ExecutorService)

通过 Executors 工具类或 ThreadPoolExecutor 创建线程池。

// 固定大小线程池
ExecutorService fixedPool = Executors.newFixedThreadPool(3);

// 执行无返回值任务
fixedPool.execute(() -> System.out.println("Runnable 任务"));

// 执行有返回值任务
Future<String> future = fixedPool.submit(() -> {
    Thread.sleep(500);
    return "Callable 结果";
});

System.out.println(future.get()); // 获取结果

// 关闭线程池
fixedPool.shutdown(); // 不再接受新任务,等待已提交任务完成

生产级线程池创建(推荐):

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2,                    // corePoolSize
    5,                    // maximumPoolSize
    60L,                  // keepAliveTime
    TimeUnit.SECONDS,     // 时间单位
    new LinkedBlockingQueue<>(100), // 任务队列
    new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

优点: ① 避免频繁创建/销毁线程的开销(复用线程);② 方便管理线程生命周期;③ 提供丰富的配置参数
缺点: 需合理配置线程池参数,否则可能引发问题

三、本质分析

flowchart TD
    A[创建线程的底层本质] --> B["new Thread() 创建Thread对象"]
    B --> C["thread.start() 启动线程"]
    C --> D["JVM调用thread.run()"]
    D --> E{run()方法来源}
    E -->|继承Thread重写| F["自定义run()"]
    E -->|构造时传入Runnable| G["target.run()"]
    E -->|传入FutureTask| H["FutureTask.run() → call()"]

    subgraph 线程池内部
        I[ThreadPoolExecutor] --> J[Worker线程]
        J --> K[Worker.runWorker]
        K --> L["while(getTask() != null) { task.run() }"]
        L -.-> M["本质仍是 Thread + Runnable"]
    end

    H -.-> N["FutureTask implements Runnable"]
    N -.-> G

四、核心对比

对比维度 Thread Runnable Callable 线程池
返回值 ❌ 无 ❌ 无 ✅ 有(泛型) ✅ 通过submit
抛异常 ❌ 不能 ❌ 不能 ✅ 可以 ✅ 可通过Future
实现方式 继承 接口 接口 工厂/构造器
代码耦合 高(耦合) 低(分离) 低(分离) 低(分离)
复用性 低(无法重复start) 极高(任务队列)
复杂度 简单 简单 中等 较复杂
适用场景 快速测试/小应用 任务与线程分离 需要异步结果 生产环境

五、开发建议

flowchart TD
    START[需要创建线程] --> Q1{有返回值?}
    Q1 -->|| CALLABLE[Callable + FutureTask  线程池submit]
    Q1 -->|| Q2{需要线程池?}
    Q2 -->|| POOL[线程池 ExecutorService]
    Q2 -->|| RUNNABLE[Runnable + Lambda]
    CALLABLE --> POOL
    RUNNABLE --> POOL

六、面试常见问题

Q1:为什么不推荐直接继承 Thread?
① Java 单继承限制,继承 Thread 后无法继承其他类;② 任务逻辑与线程耦合,不利于复用和测试;③ 线程池无法直接接受 Thread 子类(线程池接受 Runnable/Callable)。

Q2:Callable 和 Runnable 的区别?
Callable 有泛型返回值,call() 可以抛受检异常;Runnable 的 run() 无返回值且不能抛受检异常。Callable 通常配合 ExecutorService.submit()FutureTask 使用。

Q3:Future.get() 的注意事项?
get() 会阻塞当前线程直到任务完成;② 需处理 InterruptedExceptionExecutionException;③ 建议使用带超时的 get(long timeout, TimeUnit unit) 避免无限阻塞;④ 调用 cancel(true) 可中断正在执行的任务。

Q4:线程池 submit() 和 execute() 的区别?
execute(Runnable) 无返回值;submit(Callable/Runnable) 返回 Future 对象。submit 内部的 Runnable 也会被包装成 Callable(返回 null)。

>

notify()和notifyAll()的区别详解:线程唤醒机制与信号丢失问题

notify()和notifyAll()的区别详解:线程唤醒机制与信号丢失问题

一、定义

notify()notifyAll() 都是 Object 类的 native 方法,用于唤醒正在等待该对象监视器锁的线程。它们的区别在于被唤醒的线程数量不同:

  • notify():随机唤醒一个在该对象上 wait() 的线程
  • notifyAll():唤醒所有在该对象上 wait() 的线程

二、原理分析

对象内部等待机制

每个 Java 对象内部维护两个队列:
1. Wait Set(等待集):调用 wait() 的线程进入此队列
2. Entry Set(锁竞争队列):未能获取到锁的线程进入此队列

flowchart TD
    subgraph 对象监视器
        WS[Wait Set 等待集]
        ES[Entry Set 锁竞争队列]
        Owner[当前锁持有者]
    end

    Owner -->|"wait()"| WS
    WS -->|"notify()"| ES_ONE["随机一个线程 → Entry Set"]
    WS -->|"notifyAll()"| ES_ALL["所有线程 → Entry Set"]
    ES_ONE -.->|"重新竞争锁"| Owner
    ES_ALL -.->|"全部竞争锁"| Owner

    style WS fill:#f9f,stroke:#333
    style Owner fill:#9cf,stroke:#333

notify() 的过程

flowchart LR
    A["线程调用 obj.notify()"] --> B["从 Wait Set 中随机选一个线程W"]
    B --> C["将线程W移出 Wait Set"]
    C --> D["将线程W加入 Entry Set"]
    D --> E["线程W状态: WAITING → BLOCKED"]
    E --> F["线程W参与锁竞争"]
    F --> G{竞争是否成功}
    G -->|"成功"| H["线程W继续执行"]
    G -->|"失败"| I["线程W保持在 BLOCKED"]

notifyAll() 的过程

flowchart LR
    A["线程调用 obj.notifyAll()"] --> B["将 Wait Set 中所有线程移出"]
    B --> C["所有线程加入 Entry Set"]
    C --> D["所有线程状态: WAITING → BLOCKED"]
    D --> E["全部参与锁竞争(其中一个成功)"]
    E --> F{其他线程}
    F -->|"未抢到锁"| G["保持在 BLOCKED
等待下次释放"
] F -->|"抢到锁"| H["继续执行"]

三、核心区别对比

对比维度 notify() notifyAll()
唤醒数量 1个(随机选择) 全部
性能 较高(只需移动1个线程) 较低(需要移动所有线程)
安全性 较低(可能信号丢失) 较高(所有线程都收到信号)
适用场景 所有等待线程条件相同(如同一优先级) 多个线程等待不同条件
唤醒策略 JVM 实现决定,通常是最早等待的线程 全部唤醒
锁竞争 1个线程参与竞争 所有线程参与竞争(除1个外继续阻塞)

四、代码示例

notify() 信号丢失演示

/**
 * notify() 可能导致的信号丢失(非确定性行为)
 */
public class NotifyLostSignalDemo {

    private static final Object LOCK = new Object();
    private static boolean condition = false;

    public static void main(String[] args) throws InterruptedException {
        Thread waiter = new Thread(() -> {
            synchronized (LOCK) {
                // 推荐写法:使用 while 循环检查条件!
                if (!condition) { // ⚠️ 应该用 while 而非 if
                    try {
                        System.out.println("等待者:条件不满足,wait()");
                        LOCK.wait();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
                System.out.println("等待者:被唤醒");
            }
        });

        Thread notifier = new Thread(() -> {
            synchronized (LOCK) {
                condition = true;
                LOCK.notify(); // 只唤醒一个
                System.out.println("通知者:已发出 notify()");
            }
        });

        notifier.start(); // 先发通知
        Thread.sleep(100);
        waiter.start();   // 后等待 → 错过信号!永远等待
    }
}

notifyAll() 正确使用

/**
 * 使用 notifyAll() + while 循环检查条件
 * 这是线程等待的标准安全模式
 */
public class NotifyAllCorrectDemo {

    private static final Object LOCK = new Object();
    private static int product = 0;
    private static final int MAX = 5;

    public static void main(String[] args) {
        // 多个消费者
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                while (true) {
                    synchronized (LOCK) {
                        while (product <= 0) { // while! 而非 if
                            try {
                                LOCK.wait();
                            } catch (InterruptedException e) {
                                Thread.currentThread().interrupt();
                            }
                        }
                        product--;
                        System.out.println(Thread.currentThread().getName() +
                            " 消费,剩余: " + product);
                        LOCK.notifyAll(); // 唤醒所有等待者
                    }
                    Thread.yield();
                }
            }, "消费者-" + i).start();
        }

        // 生产者
        new Thread(() -> {
            while (true) {
                synchronized (LOCK) {
                    while (product >= MAX) {
                        try {
                            LOCK.wait();
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                    }
                    product++;
                    System.out.println("生产者生产,当前: " + product);
                    LOCK.notifyAll();
                }
                Thread.yield();
            }
        }).start();
    }
}

五、要点总结

规则 说明
必须持有锁 notify/notifyAll 必须在 synchronized 块中调用
while 而非 if 唤醒后应使用 while 再次检查条件(假唤醒问题)
优先 notifyAll 除非你能确定所有等待线程条件完全一致
notify 有风险 如果唤醒的线程恰好不需要该条件,会发生信号丢失
释放锁时机 notify 本身不释放锁,只有退出 synchronized 块后才释放

六、面试常见问题

Q1:为什么 wait() 总应该放在 while 循环中?
原因有二:①假唤醒(Spurious Wakeup):操作系统允许线程在未被 notify、notifyAll、interrupt 的情况下被唤醒(虽然实践很少,但规范允许);② 条件未满足:notify() 可能唤醒了不满足条件的线程,该线程需要重新检查条件。使用 while(condition) { wait(); } 可以同时解决这两个问题。

Q2:notify() 唤醒的线程是随机的吗?
取决于 JVM 实现。HotSpot VM 的实现中,notify() 从 Wait Set 中移出一个线程,顺序取决于内部实现(通常与等待时间有关,但不保证公平性)。生产代码不应依赖唤醒顺序。

Q3:为什么说 notifyAll() 比 notify() 更安全?
假设多个线程在同一个对象上等待不同条件(线程A等条件X,线程B等条件Y)。如果条件X满足时调用 notify(),可能唤醒了正在等条件Y的线程B,导致线程B检查条件Y不满足后重新等待,而正确唤醒线程A的机会就丢失了。notifyAll() 能保证所有等待线程都能检查自己的条件。

Q4:notify/notifyAll 与 Lock 中 Condition 的 signal/signalAll 有什么区别?
功能相似,但 Condition 有几点优势:① 可以为同一把锁创建多个等待条件(Condition),避免 notifyAll 的性能开销;② 支持公平唤醒策略;③ 支持超时和中断。

>

synchronized关键字用法详解:修饰方法、代码块与静态方法

synchronized关键字用法详解:修饰方法、代码块与静态方法

一、定义

synchronized 是 Java 提供的内置锁(Intrinsic Lock / Monitor Lock)机制,用于保证原子性可见性有序性。它可以修饰方法或代码块,确保同一时刻只有一个线程执行被保护的代码段。

二、三种用法详解

1. 修饰实例方法

锁住的是当前实例对象this)。

public class Counter {
    private int count = 0;

    // 等价于 synchronized(this) { count++; }
    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

特点:
– 锁对象是 this(调用该方法的实例)
– 不同实例互不影响(每个实例有自己的锁)
– 同一个实例的多个 synchronized 方法互斥

2. 修饰静态方法

锁住的是当前类的 Class 对象Counter.class)。

public class Counter {
    private static int totalCount = 0;

    // 等价于 synchronized(Counter.class) { totalCount++; }
    public static synchronized void incrementTotal() {
        totalCount++;
    }

    public static synchronized int getTotalCount() {
        return totalCount;
    }
}

特点:
– 锁对象是 Class 对象(全局唯一)
– 所有实例共享同一个类锁
– 静态 synchronized 方法与实例 synchronized 方法互不干扰(锁不同)

3. 修饰代码块

可以灵活指定锁对象,比方法更精细的控制粒度。

public class BankAccount {
    private final Object lock = new Object(); // 专门的锁对象
    private long balance;

    public void transfer(long amount) {
        // 非同步操作
        System.out.println("准备转账...");

        // 只保护关键代码
        synchronized (lock) {
            if (balance >= amount) {
                balance -= amount;
            } else {
                throw new RuntimeException("余额不足");
            }
        }

        // 非同步操作
        System.out.println("转账完成");
    }
}

特点:
– 锁对象可以是任意 Object 实例
– 粒度更细,可以部分同步
– 推荐使用专用的 private final Object lock,不与外部共享锁对象

三、锁的选择策略

flowchart TD
    A[需要同步] --> B{作用范围}
    B -->|"保护静态变量"| C["静态方法锁
Class对象"
] B -->|"保护实例变量"| D{是否需要细粒度} D -->|"整个方法"| E["实例方法锁
this"
] D -->|"局部同步"| F["代码块锁
this 或 专用锁对象"
] E -->|"多个同步方法互斥"| G["用 this 作为锁"] F -->|"需要更多控制"| H["使用私有锁对象
private final Object"
]

四、可重入特性

synchronized 具有可重入性:同一个线程在持有锁期间可以再次获取同一把锁。

public class ReentrantDemo {

    public synchronized void outer() {
        System.out.println("outer");
        inner(); // 同一个线程可以再次获取锁
    }

    public synchronized void inner() {
        System.out.println("inner");
    }

    public static void main(String[] args) {
        new ReentrantDemo().outer(); // 正常执行,不会死锁
    }
}

五、三种用法的对比

用法 锁对象 锁粒度 适用场景
修饰实例方法 this(当前实例) 整个方法 保护实例变量,方法较简单
修饰静态方法 类名.class(Class对象) 整个方法 保护静态变量,工具类方法
修饰代码块 任意 Object 代码块 只需要同步局部操作,提升性能

六、完整代码示例

/**
 * synchronized 三种用法综合演示
 */
public class SynchronizedUsageDemo {

    private static int staticCounter = 0;
    private int instanceCounter = 0;
    private final Object lock = new Object();

    // 1. 实例方法同步
    public synchronized void incInstanceMethod() {
        instanceCounter++;
    }

    // 2. 静态方法同步
    public static synchronized void incStaticMethod() {
        staticCounter++;
    }

    // 3. 同步代码块 - 使用this
    public void incThisBlock() {
        synchronized (this) {
            instanceCounter++;
        }
    }

    // 4. 同步代码块 - 使用Class对象
    public void incClassBlock() {
        synchronized (SynchronizedUsageDemo.class) {
            staticCounter++;
        }
    }

    // 5. 同步代码块 - 使用私有锁对象(推荐方式)
    public void incPrivateLock() {
        synchronized (lock) {
            instanceCounter++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedUsageDemo demo = new SynchronizedUsageDemo();
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                demo.incPrivateLock();
                incStaticMethod();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("instanceCounter: " + demo.instanceCounter); // 2000
        System.out.println("staticCounter: " + staticCounter);           // 2000
    }
}

七、注意事项

注意点 说明
锁对象不可变 锁对象不能为 null,不宜被修改为其他对象
包装类型陷阱 Integer、Long 等包装类型作为锁时,自动装箱/拆箱会改变对象引用
String 常量池 字符串字面量作为锁可能被其他代码意外共享
异常释放锁 同步块内抛出异常会自动释放锁
性能 代码块同步优于方法同步(粒度更细)
锁粗化 JVM 会自动合并相邻的同步块(锁粗化优化)

八、面试常见问题

Q1:实例方法 synchronized 和静态方法 synchronized 互斥吗?
不互斥。实例方法锁的是 this(实例对象),静态方法锁的是 Class 对象(类对象)。两者是不同的锁对象,所以不会互斥。

Q2:synchronized 代码块中使用字符串字面量作为锁有什么问题?
字符串字面量会被 JVM 放入常量池。如果不同类中使用了相同的字符串字面量作为锁,它们实际上是同一个对象,可能造成意外的锁竞争。

// ❌ 不推荐
synchronized ("LOCK") { }

// ✅ 推荐 — 使用私有锁对象
private final Object lock = new Object();
synchronized (lock) { }

Q3:synchronized 和 Lock 如何选择?
简单场景用 synchronized(代码简洁、不易出错);复杂场景如公平锁、读写分离、可中断等待、多条件等待等,用 Lock。

>

synchronized底层原理详解:对象头、Monitor锁与锁升级

synchronized底层原理详解:对象头、Monitor锁与锁升级

一、定义

synchronized 的底层实现依赖于 Java 对象头(Object Header) 中的 Mark WordMonitor(管程/监视器锁)。JDK 6 之后引入了锁升级机制,从无锁 → 偏向锁 → 轻量级锁 → 重量级锁,大幅优化了性能。

二、Java 对象头

每个 Java 对象在内存中除了实例数据外,还有对象头。32位 JVM 的对象头结构如下:

┌────────────────────────────────────────────┐
│             Mark Word (32 bits)             │
├──────────┬─────────────────────────────────┤
│ 标识字段  │        状态标记                  │
├──────────┼─────────────────────────────────┤
│ 25 bit   │ 4bit           │ 2bit │ 1bit    │
│ 哈希码   │ GC分代年龄      │ 锁标志│偏向位   │
├──────────┴─────────────────────────────────┤
│             Klass Pointer (32 bits)         │
│          指向方法区的类元数据                │
├────────────────────────────────────────────┤
│             实例数据(非对象头部分)          │
└────────────────────────────────────────────┘

Mark Word 在不同锁状态下的结构

flowchart TD
    subgraph 32位MarkWord
        A["无锁(01)
hash:25 | age:4 | 0|01"] B["偏向锁(01)
thread:23 | epoch:2 | age:4 | 1|01"] C["轻量级锁(00)
ptr_to_lock_record:30"] D["重量级锁(10)
ptr_to_monitor:30"] E["GC标记(11)
空"] end

三、Monitor 机制

每个 Java 对象都关联一个 Monitor(监视器),在 HotSpot 中由 ObjectMonitor 实现:

// HotSpot ObjectMonitor 简化结构
ObjectMonitor {
    _header;       // 对象头
    _count;        // 重入计数
    _owner;        // 持有锁的线程
    _WaitSet;      // 调用 wait() 的线程队列
    _EntryList;    // 等待获取锁的线程队列
    _recursions;   // 锁重入次数
}

Monitor 工作流程

flowchart TD
    subgraph 锁的获得
        A["线程尝试进入 synchronized"] --> B{判断_owner}
        B -->|"_owner == null"| C["设置_owner = 当前线程
_count = 1"
] B -->|"_owner == 当前线程"| D["_count++
可重入"
] B -->|"_owner == 其他线程"| E["线程进入_EntryList
状态: BLOCKED"
] end subgraph 锁的释放 F["线程退出 synchronized"] --> G["_count--"] G --> H{_count == 0?} H -->|"否"| I["仍然持有锁"] H -->|"是"| J["_owner = null"] J --> K["唤醒_EntryList中的线程
或_EntryList为空则等待"
] end

四、锁升级(锁膨胀)

JDK 6 引入的锁升级机制是 synchronized 性能优化的核心。

flowchart LR
    A["无锁"] -->|"有线程访问"| B["偏向锁"]
    B -->|"其他线程竞争"| C["轻量级锁
CAS自旋"
] C -->|"自旋超过阈值
或等待线程过多"
| D["重量级锁
OS互斥量"
] B -.->|"达到安全点(全局无竞争)"| A["偏向锁撤销
回到无锁"
]

各锁状态详解

锁状态 工作原理 开销 适用场景
无锁 对象未被任何线程锁定 初始状态
偏向锁 锁记录持有线程ID,同一线程再次获取无需CAS 极低 只有一个线程访问同步块
轻量级锁 使用 CAS 自旋尝试获取锁 低(用户态) 少量线程、短时间竞争
重量级锁 线程进入 EntryList 阻塞,依赖 OS 互斥量 高(用户态/内核态切换) 多线程长时间竞争

锁升级代码演示

/**
 * 锁升级演示 - 通过 JOL (Java Object Layout) 观察对象头
 * 需要依赖:org.openjdk.jol:jol-core
 */
public class LockUpgradeDemo {

    public static void main(String[] args) throws InterruptedException {
        // JVM 启动后偏向锁有 4s 延迟(-XX:BiasedLockingStartupDelay=4000)
        Thread.sleep(5000);

        Object lock = new Object();
        System.out.println("偏向锁:" +
            ClassLayout.parseInstance(lock).toPrintable());

        synchronized (lock) {
            System.out.println("偏向锁(线程获取后):" +
                ClassLayout.parseInstance(lock).toPrintable());
        }

        // 模拟竞争 - 第二个线程试图获取锁
        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("轻量级锁/重量级锁:" +
                    ClassLayout.parseInstance(lock).toPrintable());
            }
        });
        t2.start();
        t2.join();
    }
}

五、字节码层面

synchronized 代码块在字节码层面通过 monitorentermonitorexit 指令实现:

public void demo() {
    synchronized (this) {
        System.out.println("hello");
    }
}

字节码:

0: aload_0
1: dup
2: astore_1
3: monitorenter         // 获取锁
4: getstatic #7         // System.out
7: ldc #13              // "hello"
9: invokevirtual #15    // println
12: aload_1
13: monitorexit         // 释放锁(正常路径)
14: goto 20
17: aload_1
18: monitorexit         // 释放锁(异常路径)
19: athrow
20: return

synchronized 方法则通过 ACC_SYNCHRONIZED 标志实现,JVM 根据该标志自动加锁/解锁。

六、要点总结表格

对比项 偏向锁 轻量级锁 重量级锁
加锁方式 记录线程ID(CAS) CAS自旋 OS互斥量
锁释放 全局安全点撤销 CAS释放 OS释放
线程阻塞 不阻塞 自旋(不阻塞) 阻塞(挂起)
性能特点 极低开销 低开销(短时有效) 高开销
适用场景 单线程访问 少量短时竞争 大量长时竞争
默认开启

七、面试常见问题

Q1:synchronized 锁升级可以降级吗?
理论上锁只能升级不能降级。不过偏向锁在遇到全局安全点(如批量撤销)时可以被撤销到无锁状态,但这不算真正的降级。轻量级锁和重量级锁一旦升级后不会回退。

Q2:偏向锁默认延迟 4 秒的原因是什么?
JVM 启动初期有大量对象创建,如果立即启用偏向锁,频繁的偏向锁撤销会增加性能开销。延迟 4 秒后,大部分对象已经初始化完成,此时启用偏向锁效率更高。可通过 -XX:BiasedLockingStartupDelay=0 关闭延迟。

Q3:什么是锁消除?
JVM 的 JIT 编译器通过逃逸分析判断同步块中的锁对象不会被其他线程访问到,就会直接消除 monitorenter/monitorexit 指令。例如 StringBuffer 的 append() 方法在局部变量中使用时,会被编译器消除同步。

Q4:锁粗化是什么?
JVM 会将相邻的多个同步块合并为一个更大范围的同步块,减少频繁加锁/解锁的开销。

// JIT 会将以下代码优化为更大的同步块
for (int i = 0; i < 100; i++) {
    synchronized (list) {
        list.add(i);
    }
}
// → 优化为:
synchronized (list) {
    for (int i = 0; i < 100; i++) {
        list.add(i);
    }
}

>

volatile关键字作用详解:可见性、禁止指令重排与内存屏障

volatile关键字作用详解:可见性、禁止指令重排与内存屏障

一、定义

volatile 是 Java 提供的最轻量级的同步机制,用于修饰变量,保证该变量的可见性有序性,但不保证原子性。它是 JMM(Java 内存模型)中的核心关键字。

二、两大核心作用

1. 保证可见性

当一个线程修改 volatile 变量的值,新值会立即写回主内存(Main Memory),其他线程读取时从主内存重新读取,从而保证每个线程看到的都是最新值。

flowchart TD
    subgraph 普通变量
        A1["线程A
修改变量 x=1"
] -->|"写入CPU缓存
不立即写回主存"
| A2["线程B
读取 x
读到旧值 0"
] end subgraph volatile变量 B1["线程A
修改 volatile y=1"
] -->|"强制写入主存"| B2["内存屏障
StoreLoad
StoreStore"
] B2 --> B3["主内存 y=1"] B3 -->|"其他线程缓存失效
强制从主存读取"
| B4["线程B
读取 y
一定读到 1"
] end

背景: 每个线程有独立的工作内存(CPU 高速缓存),普通变量修改后不会立即刷回主内存,其他线程可能读到旧值。

public class VolatileVisibilityDemo {

    private volatile boolean flag = false;

    public void test() throws InterruptedException {
        new Thread(() -> {
            System.out.println("等待 flag 为 true...");
            while (!flag) { /* 如果不加 volatile,这里可能永远无法看到 flag 变化 */ }
            System.out.println("检测到 flag 变化,退出");
        }, "reader").start();

        Thread.sleep(1000);
        flag = true; // 写 volatile 变量
        System.out.println("主线程已将 flag 设为 true");
    }
}

2. 禁止指令重排序

JVM 为了优化性能可能对指令进行重排序,volatile 通过插入内存屏障(Memory Barrier) 来阻止特定类型的重排序。

flowchart LR
    subgraph volatile写
        A["普通写操作"] --> B["StoreStore屏障"]
        B --> C["volatile写"]
        C --> D["StoreLoad屏障"]
        D --> E["后续读写操作"]
    end

    subgraph volatile读
        F["普通读写操作"] --> G["volatile读"]
        G --> H["LoadLoad屏障"]
        H --> I["LoadStore屏障"]
        I --> J["后续读写操作"]
    end

经典应用:DCL(Double-Checked Locking)单例模式

public class Singleton {
    // volatile 禁止指令重排:防止 instance = new Singleton() 的指令重排序
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {               // 第一次检查(无锁)
            synchronized (Singleton.class) {  // 加锁
                if (instance == null) {       // 第二次检查(双检)
                    instance = new Singleton(); // ★ 关键点
                }
            }
        }
        return instance;
    }
}

instance = new Singleton() 在字节码层面分三步:
1. memory = allocate() — 分配内存空间
2. initSingleton(memory) — 初始化对象
3. instance = memory — 将引用指向内存(此时 instance != null)

如果步骤 2 和 3 被重排序(先赋值再初始化),其他线程可能在第一次检查时看到 instance != null,直接返回未初始化完成的对象。volatile 禁止了这个重排序

三、volatile 不保证原子性

public class VolatileNonAtomicDemo {

    private volatile int count = 0;

    public void test() throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                count++; // 非原子操作:读-改-写
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("count = " + count); // 通常 < 20000
    }
}

四、Happens-Before 规则

volatile 遵循 volatile 变量规则:对一个 volatile 变量的操作,happens-before 于后续对该变量的操作。

flowchart LR
    subgraph 线程A
        A1["volatile 写
flag = true"
] end subgraph 线程B B1["volatile 读
读取 flag"
] B2["可见线程A写之前的所有操作"] end A1 -- "happens-before" --> B1 A1 -.->|"在此之前的所有写入"| A2["普通变量赋值..."] A2 -.->|"也可见"| B1

五、volatile vs synchronized

对比维度 volatile synchronized
可见性 ✅ 保证(强制读写主存) ✅ 保证(解锁前自动刷回主存)
原子性 不保证 ✅ 保证
有序性 ✅ 禁止指令重排(内存屏障) ✅ 通过互斥实现
适用对象 变量 方法或代码块
线程阻塞 ❌ 不会阻塞 ✅ 可能阻塞(重量级锁)
性能开销 低(仅内存屏障) 较高
使用场景 状态标记(DCL) 复合操作、原子性需求

六、volatile 的适用场景

flowchart TD
    A["需要线程同步"] --> B{需要原子性?}
    B -->|"是,复合操作"| C["synchronized / Lock / Atomic类"]
    B -->|"否,只有可见性需求"| D{操作类型}
    D -->|"状态标记(boolean flag)"| E["volatile ✅"]
    D -->|"DCL单例"| E
    D -->|"发布安全
(如不可变对象引用)"
| E D -->|"计数器/累加操作"| C

七、面试常见问题

Q1:volatile 能解决 i++ 的线程安全问题吗?
不能。i++ 是”读-改-写”三步操作(load → add → store),volatile 只保证每次读到的值是最新的,但三步之间可能被其他线程打断,导致写覆盖。解决 i++ 的方式:synchronizedAtomicIntegerReentrantLock

Q2:volatile 底层的内存屏障具体是怎样的?
volatile 写前StoreStore 屏障(禁止与前面写操作重排序)
volatile 写后StoreLoad 屏障(强制刷主存 + 禁止与后面读操作重排序)
volatile 读后LoadLoad 屏障 + LoadStore 屏障

StoreLoad 是最”昂贵”的屏障,因为它需要等待所有存储操作完成,并且使后续读取失效。

Q3:什么是 happens-before 规则?除了 volatile 还有哪些?
Happens-before 是 JMM 定义的内存可见性保证。主要规则包括:
1. 程序次序规则:同一线程中,写在前面的操作 happens-before 后面的操作
2. volatile 变量规则:volatile 写 happens-before 后续读
3. 锁规则:解锁 happens-before 加锁
4. 传递性:A happens-before B, B happens-before C → A happens-before C
5. 线程启动规则start() happens-before 该线程任何动作
6. 线程终止规则:线程中所有操作 happens-before join() 返回

Q4:long 和 double 的读写是不是原子操作?
在 32 位 JVM 上,long 和 double 的 64 位读写可能被拆分为两个 32 位操作,不是原子操作。用 volatile 修饰后保证其原子性(volatile 保证 64 位变量读写原子性)。

>

volatile和synchronized区别详解:同步机制的对比与选择

volatile和synchronized区别详解:同步机制的对比与选择

一、定义

volatilesynchronized 是 Java 中最常用的两个同步关键字。虽然它们都与线程安全相关,但设计目标、使用方式和适用场景有根本性的不同。

  • volatile:最轻量级的同步机制,解决可见性有序性问题
  • synchronized:重量级的互斥同步机制,解决可见性有序性原子性问题

二、核心区别对比

对比维度 volatile synchronized
原子性 不保证 ✅ 保证(代码块内所有操作)
可见性 ✅ 保证(强制写回主存+缓存失效) ✅ 保证(解锁前自动刷回主存)
有序性 ✅ 禁止指令重排(内存屏障) ✅ 通过互斥实现(串行化执行)
修饰对象 变量(field) 方法、代码块、对象
是否阻塞 不会阻塞线程 ✅ 线程可能进入BLOCKED状态
编译优化 阻止编译器和 JIT 重排 允许重排(互斥已保证正确性)
线程中断 不支持 不直接支持(Lock支持)
性能开销 低(仅插入内存屏障) 较高(用户态→内核态切换)
底层实现 内存屏障(Lock指令) Monitor(monitorenter/monitorexit)

三、原理对比

volatile 的底层

flowchart TD
    A["volatile 变量写"] --> B["StoreStore 屏障
禁止前面写操作重排"
] B --> C["StoreLoad 屏障
强制刷主存
使其他CPU缓存行失效"
] C --> D["其他线程读取时
必须从主存加载"
] E["volatile 变量读"] --> F["LoadLoad 屏障
禁止后面读重排"
] F --> G["LoadStore 屏障
禁止后面写重排"
] G --> H["从主存读取最新值"]

synchronized 的底层

flowchart TD
    A["synchronized 代码块"] --> B["monitorenter
获取 Monitor 锁"
] B --> C["执行临界区代码"] C --> D["monitorexit
释放 Monitor 锁"
] D --> E["修改刷回主内存
(缓存失效)"
] B -.->|"锁升级"| F["偏向锁 → 轻量级锁 → 重量级锁"]

四、使用场景选择

flowchart TD
    START["遇到线程安全问题"] --> Q1{"需要原子性?
(如 count++ 复合操作)"
} Q1 -->|"是"| SYN["synchronized / Lock / Atomic类"] Q1 -->|"否"| Q2{"需要互斥阻塞?
(保证同一时刻只有一个线程)"
} Q2 -->|"是"| SYN Q2 -->|"否"| Q3{"操作类型"} Q3 -->|"状态标记
boolean flag / volatile变量"
| VOL["volatile ✅"] Q3 -->|"DCL单例模式"| VOL Q3 -->|"发布不可变对象引用"| VOL Q3 -->|"计数器/累加"| SYN

五、代码对比示例

/**
 * volatile 和 synchronized 对比演示
 */
public class VolatileVsSyncDemo {

    // volatile 变量
    private volatile boolean flag = false;
    // synchronized 保护的变量
    private int count = 0;

    // volatile 场景1:状态标记(合适)
    public void volatileFlagExample() throws InterruptedException {
        Thread worker = new Thread(() -> {
            while (!flag) { // 轮询等待
                // 如果不用 volatile,这里可能死循环
            }
            System.out.println("检测到 flag 变化");
        });

        worker.start();
        Thread.sleep(1000);
        flag = true; // 主线程修改
    }

    // volatile 场景2:累加(不合适——无法保证原子性)
    private volatile int volatileCount = 0;

    public void volatileIncrement() throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) volatileCount++;
        };
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("volatile 累加结果: " + volatileCount); // 通常 < 20000
    }

    // synchronized 场景:累加(合适——保证原子性)
    public synchronized void syncIncrement() {
        count++;
    }

    public void testSyncIncrement() throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) syncIncrement();
        };
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("synchronized 累加结果: " + count); // 保证 20000
    }
}

六、典型场景分析

场景 推荐方式 原因
状态标志(boolean/flags) volatile 只有可见性需求,无需原子性
一次性安全发布(DCL单例) volatile 防止指令重排导致对象未初始化
计数器(count++) synchronizedAtomicInteger 需要原子性
复合操作(check-then-act) synchronizedLock 需要原子性+可见性
发布不可变对象 volatile 不可变对象引用本身是安全的
缓存池/累加器 synchronizedLongAdder 高并发下的性能优化

DCL 单例对比

// ❌ 错误 — 没有 volatile
public class Singleton {
    private static Singleton instance; // 缺少 volatile!

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // 可能被重排序!
                }
            }
        }
        return instance; // 可能返回未初始化完成的对象
    }
}

// ✅ 正确 — volatile 禁止重排序
public class Singleton {
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

七、面试常见问题

Q1:什么时候用 volatile 什么时候用 synchronized?
需要原子性(复合操作)或互斥访问用 synchronized;只有可见性需求(如状态标志)或禁止指令重排(如 DCL)用 volatile

Q2:volatile 能替代 synchronized 吗?
绝大多场景不能volatile 只能保证变量层面的可见性和有序性,synchronized 能保证代码块级别的原子性、可见性和有序性。但”单一 volatile 变量的读写”这种简单场景可以替代。

Q3:两者在性能上差多少?
volatilesynchronized 轻量 1-2 个数量级:volatile 仅插入内存屏障(几十纳秒开销),synchronized 涉及锁升级、可能线程挂起(微秒到毫秒级别)。但在 JDK 8+ 锁优化后,无竞争时 synchronized 的性能已接近 volatile。

Q4:synchronized 和 volatile 能一起用吗?
可以。典型例子就是 DCL 中,最外层 if (instance == null) 读取 volatile 变量(利用 volatile 的可见性),内部使用 synchronized 保证创建实例的原子性。

Q5:为什么 synchronized 能保证可见性?
因为 synchronized 有 lock 和 unlock 语义。lock 时清空工作内存、从主内存中重新读取变量;unlock 前将工作内存中修改的变量刷回主内存。所以 synchronized 也能保证可见性。

>

Lock接口和synchronized区别详解:显式锁与隐式锁的全面对比

Lock接口和synchronized区别详解:显式锁与隐式锁的全面对比

一、定义

Lockjava.util.concurrent.locks 包下的接口,提供了比 synchronized灵活、功能更丰富的锁机制。synchronized 是 Java 语言内置的隐式锁,而 Lock 是 JDK 实现的显式锁。

二、核心区别对比

对比维度 synchronized Lock
关键字 vs 接口 Java 关键字(语言层面) 接口(代码层面),JDK 实现
锁获取方式 隐式获取,进入同步块自动获取 显式获取,需调用 lock() / tryLock()
锁释放方式 隐式释放,退出同步块自动释放 显式释放,必须调用 unlock()(通常放 finally)
公平性 非公平锁(无法配置) 支持公平锁和非公平锁
响应中断 ❌ 不支持(阻塞线程无法中断) ✅ 支持(lockInterruptibly())
超时获取 ❌ 不支持 ✅ 支持(tryLock(timeout, unit))
多条件 一个对象只能有一个 Condition 可以创建多个 Condition 对象
锁状态查询 ❌ 无法查询 ✅ 可查询是否被持有、等待线程数等
性能 JDK 6 后大幅优化,无竞争时接近 Lock 灵活但代码更复杂
异常处理 自动释放,不会死锁 必须在 finally 中 unlock,否则可能死锁

三、原理对比

synchronized 的等待/通知模型

flowchart LR
    subgraph 一个Monitor
        LOCK["锁对象"]
        WS["WaitSet
一个等待队列"
] ES["EntryList
一个阻塞队列"
] end LOCK --- WS LOCK --- ES WS -.->|"单个等待条件"| ES

Lock 的多条件模型

flowchart LR
    subgraph 同一个Lock
        LOCK["ReentrantLock"]
        C1["Condition1
notFull
等待队列A"
] C2["Condition2
notEmpty
等待队列B"
] end LOCK --- C1 LOCK --- C2 C1 -.->|"A条件满足"| WA["唤醒等待A的线程"] C2 -.->|"B条件满足"| WB["唤醒等待B的线程"]

四、Lock 的核心特性详解

1. 公平锁 vs 非公平锁

// 公平锁:按线程等待时间顺序获取锁(FIFO)
Lock fairLock = new ReentrantLock(true);

// 非公平锁:允许"插队",默认方式(性能更好)
Lock unfairLock = new ReentrantLock(false); // 默认 false

/**
 * 公平锁性能略低但避免"线程饥饿"
 * 非公平锁性能更高但可能导致某些线程长时间获取不到锁
 */

2. 可中断的锁获取

Lock lock = new ReentrantLock();

public void interruptibleLock() throws InterruptedException {
    // 可中断的锁获取:等待锁的过程中支持响应中断
    lock.lockInterruptibly();
    try {
        // 临界区
    } finally {
        lock.unlock();
    }
}

3. 锁超时获取

Lock lock = new ReentrantLock();

public void tryLockWithTimeout() {
    try {
        // 尝试在 500ms 内获取锁,获取不到就放弃
        if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
            try {
                System.out.println("获取锁成功");
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("获取锁超时,执行其他逻辑");
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

4. 多条件(Multiple Conditions)

/**
 * 使用 Lock 的多个 Condition 实现更精确的线程唤醒
 */
class BoundedBuffer {
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();   // 缓冲未满
    private final Condition notEmpty = lock.newCondition();  // 缓冲非空
    private final Object[] items = new Object[100];
    private int putIndex, takeIndex, count;

    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();    // 队列满,等待"未满"条件
            items[putIndex] = x;
            if (++putIndex == items.length) putIndex = 0;
            count++;
            notEmpty.signal();      // 唤醒等待"非空"条件的消费者
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();   // 队列空,等待"非空"条件
            Object x = items[takeIndex];
            if (++takeIndex == items.length) takeIndex = 0;
            count--;
            notFull.signal();       // 唤醒等待"未满"条件的生产者
            return x;
        } finally {
            lock.unlock();
        }
    }
}

五、标准 Lock 使用模式

Lock lock = new ReentrantLock();

public void safeMethod() {
    lock.lock(); // 获取锁
    try {
        // 临界区代码
        // 可能抛出异常
    } finally {
        lock.unlock(); // 必须在 finally 中释放锁!
    }
}

六、何时选择哪个

flowchart TD
    A[需要同步访问] --> B{需要以下特性?}
    B -->|"公平锁"| LOCK["Lock"]
    B -->|"可中断锁"| LOCK
    B -->|"超时获取锁"| LOCK
    B -->|"多个等待条件"| LOCK
    B -->|"简单同步
无特殊需求"
| SYNC["synchronized"] SYNC -->|"代码简洁
不易出错
性能已优化"
| END

七、面试常见问题

Q1:为什么 Lock 必须在 finally 中释放?
因为 Lock 不会像 synchronized 那样自动释放。如果在临界区抛出异常,锁将不会被释放,导致其他线程永远无法获取该锁,造成死锁。使用 try { ... } finally { lock.unlock(); } 可以保证无论是否抛出异常,锁都会被释放。

Q2:synchronized 和 ReentrantLock 哪个性能更好?
JDK 6 之后,synchronized 在无竞争时几乎零开销(偏向锁);有竞争时 JIT 会自动优化,性能与 Lock 相当。选择时不应以性能为主要考量,而应根据功能需求:
– 需要公平锁、超时、中断 → 用 Lock
– 简单场景、读代码的人不熟悉 Lock → 用 synchronized

Q3:ReentrantLock 的可重入是什么意思?
同一个线程可以多次获取同一个锁,而不会死锁。锁内部维护一个计数器,每次获取 +1,每次释放 -1,减到 0 时才真正释放锁。

Lock lock = new ReentrantLock();
lock.lock();
lock.lock(); // 再次获取(重入计数 = 2)
try {
    // 临界区
} finally {
    lock.unlock(); // 计数 = 1
    lock.unlock(); // 计数 = 0,真正释放
}

Q4:ReadWriteLock 是什么?
ReadWriteLock 是一个特殊的 Lock,维护一对锁:读锁(共享锁)和写锁(独占锁)。多个线程可以同时读,但写时互斥(读-写互斥,写-写互斥)。适用场景:读多写少的共享数据。

>

ReentrantLock可重入锁原理详解:AQS、公平性与条件队列

ReentrantLock可重入锁原理详解:AQS、公平性与条件队列

一、定义

ReentrantLock 是 JDK 提供的可实现线程同步的显式锁,属于 java.util.concurrent.locks 包。可重入(Reentrant) 指同一线程可以多次获取同一把锁而不会死锁。它内部基于 AQS(AbstractQueuedSynchronizer) 实现。

二、可重入性原理

计数器机制

flowchart TD
    A["线程T1 lock()"] --> B{_owner == null?}
    B -->|"是"| C["_owner = T1
_state = 1"
] B -->|"_owner == T1"| D["_state++
重入: 1 → 2"
] B -->|"_owner == T2"| E["T2 进入等待队列
park() 阻塞"
] C --> F["执行临界区"] D --> F F --> G["unlock()"] G --> H["_state--"] H --> I{_state == 0?} I -->|"否"| J["锁仍然持有"] I -->|"是"| K["_owner = null"] K --> L["唤醒队列中下一个线程"]

关键点: ReentrantLock 内部维护 state 字段(AQS 的核心字段),每次 lock() +1,每次 unlock() -1。只有 state == 0 时锁才真正释放。

三、AQS(AbstractQueuedSynchronizer)原理

AQS 是 ReentrantLock 的底层基石,它维护了一个 CLH 变体双向队列和一个 state 状态值

flowchart LR
    subgraph AQS同步队列
        H["Head
哨兵节点"
] --> N1["Node1
线程T1"
] N1 --> N2["Node2
线程T2"
] N2 --> N3["Node3
线程T3"
] N3 --> T["Tail"] end subgraph 当前持有者 Owner["持有锁的线程
_owner"
] end Owner -.->|"waitStatus"| H

AQS 状态值

state 值 含义
0 锁未被任何线程持有
1 锁被某个线程持有(第一次获取)
N (N>1) 锁被某线程重入 N 次

四、公平锁 vs 非公平锁

非公平锁(默认)

ReentrantLock lock = new ReentrantLock(false); // 默认非公平

原理: 当前线程在 lock() 时会先尝试 CAS 抢锁,不检查队列。即使队列中有线程在等待,新来的线程也可以”插队”直接抢。

flowchart TD
    START["非公平锁 lock()"] --> TRY["CAS尝试 state: 0 → 1"]
    TRY -->|"成功"| GOT["获取锁"]
    TRY -->|"失败"| ACQ["acquire(1)
尝试入队等待"
] ACQ --> RETRY["再次尝试 CAS 抢锁"] RETRY -->|"成功"| GOT RETRY -->|"失败"| PARK["park() 阻塞
等待被唤醒"
]

公平锁

ReentrantLock lock = new ReentrantLock(true); // 公平锁

原理: 当前线程在 lock() 时先检查队列中是否有前驱节点。如果有,按照 FIFO 顺序排队,不允许插队。

flowchart TD
    START["公平锁 lock()"] --> CHECK{"队列中
有前驱节点?"
} CHECK -->|"有(有人排队)"| QUEUE["入队等待"] CHECK -->|"无(队列为空)"| CAS["CAS尝试 state: 0 → 1"] CAS -->|"成功"| GOT["获取锁"] CAS -->|"失败"| QUEUE

性能差异

特性 非公平锁 公平锁
吞吐量 更高(减少上下文切换) 较低(频繁线程调`度)
调度粒度 粗粒度(允许插队) 细粒度(FIFO队列)
线程饥饿 可能存在(极端情况) 不可能
适用场景 默认选择 需要保证公平性的场景

五、代码示例

import java.util.concurrent.locks.ReentrantLock;

/**
 * ReentrantLock 可重入特性演示
 */
public class ReentrantLockDemo {

    private final ReentrantLock lock = new ReentrantLock();

    public void outer() {
        lock.lock();
        try {
            System.out.println("outer 获取锁,当前重入次数: " + lock.getHoldCount());
            inner(); // 同一个线程再次获取同一把锁
        } finally {
            lock.unlock();
            System.out.println("outer 释放锁,当前重入次数: " + lock.getHoldCount());
        }
    }

    public void inner() {
        lock.lock();
        try {
            System.out.println("inner 获取锁,当前重入次数: " + lock.getHoldCount());
        } finally {
            lock.unlock();
            System.out.println("inner 释放锁,当前重入次数: " + lock.getHoldCount());
        }
    }

    public static void main(String[] args) {
        new ReentrantLockDemo().outer();
    }
}

公平性对比演示

/**
 * 公平锁 vs 非公平锁对比
 * 公平锁的等待时间更均匀,但整体吞吐量更低
 */
public class FairVsUnfairDemo {

    public static void testFairness(boolean fair) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock(fair);
        String lockType = fair ? "公平锁" : "非公平锁";

        Runnable task = () -> {
            for (int i = 0; i < 3; i++) {
                lock.lock();
                try {
                    System.out.println(lockType + " - " +
                        Thread.currentThread().getName() + " 获取锁");
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    lock.unlock();
                }
            }
        };

        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(task, "线程-" + i);
        }
        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join();
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== 非公平锁测试 ===");
        testFairness(false);
        System.out.println("\n=== 公平锁测试 ===");
        testFairness(true);
    }
}

六、Condition 条件队列

ReentrantLock 可以通过 newCondition() 创建多个条件队列,比 synchronized 的单一 wait set 更灵活。

flowchart LR
    subgraph ReentrantLock
        LOCK[Lock]
        C1["Condition
notFull"
] C2["Condition
notEmpty"
] end C1 -->|"await()"| Q1["等待队列1
生产者等待"
] C2 -->|"await()"| Q2["等待队列2
消费者等待"
] C1 -->|"signal()"| W1["唤醒队列1的线程"] C2 -->|"signal()"| W2["唤醒队列2的线程"]

七、常用的 Lock 方法

方法 描述
lock() 获取锁(阻塞直到获取成功)
tryLock() 尝试获取锁(立即返回 boolean)
tryLock(time, unit) 限时尝试获取锁
lockInterruptibly() 可中断的锁获取
unlock() 释放锁
getHoldCount() 查询当前线程的重入次数
isFair() 是否为公平锁
isLocked() 锁是否被持有

八、面试常见问题

Q1:ReentrantLock 的可重入是如何实现的?
通过 AQS 的 state 计数器。当锁持有者再次获取锁时,state++ 而不阻塞。释放时 state--,只有 state == 0 时才真正释放锁。

Q2:非公平锁比公平锁性能好的原因?
非公平锁被唤醒的线程恢复正常执行需要上下文切换,这个过程中新来的线程可以直接在用户态通过 CAS 抢锁。如果抢锁成功,就减少了一次线程挂起/唤醒的开销。因此非公平锁的吞吐量通常更高。

Q3:ReentrantLock 和 synchronized 的 lock 释放时机有什么不同?
synchronized 在退出同步块/方法时自动释放(包括异常退出);ReentrantLock 必须在 finally 中显式调用 unlock(),否则异常退出时不会解锁。

Q4:什么是锁的”公平性”?公平锁一定公平吗?
公平锁保证等待时间最长的线程最先获取锁(FIFO)。但即使使用公平锁,线程调度器并不能保证线程执行的公平性(线程获取锁后何时执行代码由 OS 调度)。公平锁保证的是锁获取顺序的公平性

>

线程池核心参数详解:ThreadPoolExecutor 的7大参数与拒绝策略

线程池核心参数详解:ThreadPoolExecutor 的7大参数与拒绝策略

一、定义

线程池的核心是 ThreadPoolExecutor 类。正确配置线程池的7大核心参数是高性能并发编程的关键。线程池解决了频繁创建/销毁线程的开销、资源无限制消耗以及线程生命周期管理等问题。

二、7大核心参数

public ThreadPoolExecutor(
    int corePoolSize,         // 1. 核心线程数
    int maximumPoolSize,      // 2. 最大线程数
    long keepAliveTime,       // 3. 存活时间
    TimeUnit unit,            // 4. 时间单位
    BlockingQueue<Runnable> workQueue, // 5. 任务队列
    ThreadFactory threadFactory,       // 6. 线程工厂
    RejectedExecutionHandler handler   // 7. 拒绝策略
);

三、参数详解

1. corePoolSize(核心线程数)

线程池中长期存活的线程数量。即使空闲也不会被回收(除非 allowCoreThreadTimeOut(true))。

2. maximumPoolSize(最大线程数)

线程池中允许的最大线程数。当任务队列满了之后,会创建新线程直至达到该上限。

3. keepAliveTime + TimeUnit(线程存活时间)

当线程数超过 corePoolSize 时,多余的空闲线程等待新任务的最长时间,超时即回收。

4. workQueue(任务队列)

当核心线程都在忙时,新任务会放入此队列等待。常用队列:

队列类型 特点 适用场景
ArrayBlockingQueue 有界队列,容量固定 控制资源消耗
LinkedBlockingQueue 可选有界/无界(默认无界) 固定线程数+无界队列
SynchronousQueue 不存储元素,直接转交 大池+小队列(CachedThreadPool)
PriorityBlockingQueue 支持优先级排序 任务有优先级需求

5. ThreadFactory(线程工厂)

用于创建新线程,可为线程指定名称、守护状态、优先级等,便于排查问题。

ThreadFactory factory = new ThreadFactory() {
    private final AtomicInteger count = new AtomicInteger(1);

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setName("业务线程池-" + count.getAndIncrement());
        t.setDaemon(false);
        return t;
    }
};

6. RejectedExecutionHandler(拒绝策略)

当线程池和任务队列都满了时,对新提交的任务执行拒绝策略。

四、任务提交流程

flowchart TD
    A["提交新任务"] --> B{"核心线程数 
corePoolSize?"
} B -->|"是"| C["创建新核心线程
执行任务"
] B -->|"否"| D{"任务队列
workQueue 未满?"
} D -->|"是"| E["放入队列等待"] D -->|"否"| F{"线程数
< maximumPoolSize?"
} F -->|"是"| G["创建新非核心线程
执行任务"
] F -->|"否"| H["执行拒绝策略"] C --> I["任务执行完毕"] G --> I E -.->|"核心线程空闲"| I I --> J{"是否超过
keepAliveTime?"
} J -->|"是(非核心线程)"| K["销毁线程"] J -->|"否"| L["等待新任务"]

五、拒绝策略详解

拒绝策略 行为 源码行为
AbortPolicy(默认) 抛出 RejectedExecutionException throw new RejectedExecutionException()
CallerRunsPolicy 由提交任务的线程直接执行 调用者线程运行任务
DiscardPolicy 直接丢弃,不抛异常 空方法
DiscardOldestPolicy 丢弃队列中最旧的任务,重试提交 从队列头部 poll 出一个任务
// 四种拒绝策略的使用
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 5, 60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(10),
    Executors.defaultThreadFactory(),
    // 可选拒绝策略:
    new ThreadPoolExecutor.AbortPolicy()           // 默认,抛异常
    // new ThreadPoolExecutor.CallerRunsPolicy()   // 调用者执行
    // new ThreadPoolExecutor.DiscardPolicy()      // 静默丢弃
    // new ThreadPoolExecutor.DiscardOldestPolicy() // 丢弃最旧
);

六、完整代码示例

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 线程池核心参数完整演示
 */
public class ThreadPoolParamDemo {

    public static void main(String[] args) {
        // 自定义线程工厂:给线程命名,便于排查
        ThreadFactory namedFactory = new ThreadFactory() {
            private final AtomicInteger count = new AtomicInteger(1);
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r, "业务线程-" + count.getAndIncrement());
                t.setUncaughtExceptionHandler((thread, e) ->
                    System.err.println(thread.getName() + " 发生异常: " + e));
                return t;
            }
        };

        // 创建自定义线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            2,                    // corePoolSize
            5,                    // maximumPoolSize
            30L,                  // keepAliveTime
            TimeUnit.SECONDS,     // unit
            new LinkedBlockingQueue<>(3), // workQueue(有界队列)
            namedFactory,         // threadFactory
            new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );

        // 提交 10 个任务(core=2, queue=3, max=5 → 最多容纳 2+3+5=10)
        // 实际最大可处理数 = corePoolSize + workQueue.capacity + (maxPoolSize - corePoolSize)
        for (int i = 1; i <= 10; i++) {
            int taskId = i;
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName() +
                    " 执行任务 " + taskId);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        executor.shutdown();
    }
}

配置合理性示例

// CPU 密集型任务(计算为主):corePoolSize ≈ CPU核心数 + 1
int cpuCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor cpuIntensive = new ThreadPoolExecutor(
    cpuCores + 1, cpuCores + 1,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<>()
);

// IO 密集型任务(读写文件/网络):corePoolSize ≈ CPU核心数 * 2
ThreadPoolExecutor ioIntensive = new ThreadPoolExecutor(
    cpuCores * 2, cpuCores * 2,
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>()
);

七、核心参数配置公式

任务类型 corePoolSize 建议 队列建议 说明
CPU密集型 CPU核心数 + 1 无界队列 减少线程切换
IO密集型 CPU核心数 × 2 有界队列 利用等待IO的时间执行其他任务
混合型 按瓶颈拆分 独立线程池 避免相互影响
高并发短任务 适量核心数 SynchronousQueue 直接转交(CachedThreadPool)

八、面试常见问题

Q1:线程池提交任务时,是先创建核心线程还是先放入队列?
先判断是否达到 corePoolSize:未达到 → 创建核心线程;已达到 → 放入任务队列;队列满了 → 创建非核心线程;达到 maximumPoolSize → 执行拒绝策略。

Q2:如何合理设置线程池参数?
根据任务性质:CPU 密集型设置 corePoolSize = N+1(N为CPU核心数),IO 密集型设置 corePoolSize = 2N。另外需要结合实际场景进行压测验证。

Q3:当核心线程空闲时,线程池会销毁它们吗?
默认情况下不会。但可以通过 executor.allowCoreThreadTimeOut(true) 开启核心线程超时回收功能(保持最小 1 个线程)。

Q4:提交任务和 execute() 和 submit() 的区别?
execute(Runnable) 无返回值;submit(Callable/Runnable) 返回 Future,可以获取执行结果或异常。submit 内部会将 Runnable 包装成 Callable。

>

常见线程池类型详解:Fixed、Cached、Scheduled、Single与自定义线程池

常见线程池类型详解:Fixed、Cached、Scheduled、Single与自定义线程池

一、定义

Executors 工具类提供了 4 种预定义的线程池快捷创建方式。但阿里巴巴 Java 开发规范不推荐使用 Executors 创建线程池,建议通过 ThreadPoolExecutor 直接创建。

二、4种常见线程池详解

1. FixedThreadPool(固定大小线程池)

ExecutorService fixedPool = Executors.newFixedThreadPool(3);
// 等价于:
new ThreadPoolExecutor(3, 3, 0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>());

特点: 核心线程数 = 最大线程数,使用无界队列 LinkedBlockingQueue

⚠️ 风险: 无界队列可能导致任务无限堆积,引发 OOM。

2. CachedThreadPool(缓存线程池)

ExecutorService cachedPool = Executors.newCachedThreadPool();
// 等价于:
new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
    new SynchronousQueue<Runnable>());

特点: 核心线程数为 0,最大线程数为 Integer.MAX_VALUE,使用 SynchronousQueue(不存任务,直接转交)。

⚠️ 风险: 最大线程数无限,高并发下可能创建海量线程,引发 OOM。

3. ScheduledThreadPool(调度线程池)

ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2);
// 等价于:
new ScheduledThreadPoolExecutor(2,
    new LinkedBlockingQueue<Runnable>());

特点: 可执行定时周期任务。

// 延迟 5 秒后执行
scheduledPool.schedule(() -> System.out.println("延迟任务"), 5, TimeUnit.SECONDS);

// 延迟 1 秒后,每隔 3 秒执行一次
scheduledPool.scheduleAtFixedRate(
    () -> System.out.println("周期任务"), 1, 3, TimeUnit.SECONDS);

// 上次执行结束后,延迟 2 秒再执行下一次
scheduledPool.scheduleWithFixedDelay(
    () -> System.out.println("固定延迟任务"), 0, 2, TimeUnit.SECONDS);

4. SingleThreadExecutor(单线程线程池)

ExecutorService singlePool = Executors.newSingleThreadExecutor();
// 等价于:
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>());

特点: 核心线程数 = 最大线程数 = 1,使用无界队列。保证任务按提交顺序执行。

ℹ️ 与 newFixedThreadPool(1) 的区别:SingleThreadExecutor 保证线程数始终为 1(不能被配置覆盖)。

三、源码结构与对比

flowchart TD
    subgraph Executors工厂方法
        FIX[newFixedThreadPool]
        CACH[newCachedThreadPool]
        SCH[newScheduledThreadPool]
        SING[newSingleThreadExecutor]
    end

    subgraph 底层实现
        TPE[ThreadPoolExecutor]
        STPE[ScheduledThreadPoolExecutor]
    end

    FIX --> TPE
    CACH --> TPE
    SING --> TPE
    SCH --> STPE
    STPE -.->|"继承自"| TPE

    TPE -.->|"内部参数不同"| P1["core=max=N
无界队列"
] TPE -.-> P2["core=0, max=MAX
SynchronousQueue"
] TPE -.-> P3["core=max=1
无界队列"
] STPE -.-> P4["core=N, max=MAX
DelayedWorkQueue"
]

四、参数对比表

线程池类型 corePoolSize maximumPoolSize keepAliveTime 任务队列 风险
FixedThreadPool N(固定) N 0 LinkedBlockingQueue(无界) 队列堆积 OOM
CachedThreadPool 0 Integer.MAX_VALUE 60秒 SynchronousQueue 线程过多 OOM
ScheduledThreadPool N Integer.MAX_VALUE 0 DelayedWorkQueue 线程过多 OOM
SingleThreadExecutor 1 1 0 LinkedBlockingQueue(无界) 队列堆积 OOM

五、完整代码示例

import java.util.concurrent.*;

/**
 * 4种内置线程池演示
 */
public class ExecutorsDemo {

    public static void main(String[] args) throws Exception {
        // 1. FixedThreadPool
        System.out.println("=== FixedThreadPool ===");
        ExecutorService fixed = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 5; i++) {
            fixed.execute(() -> {
                System.out.println(Thread.currentThread().getName());
                sleep(500);
            });
        }
        fixed.shutdown();

        // 2. CachedThreadPool
        System.out.println("=== CachedThreadPool ===");
        ExecutorService cached = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            cached.execute(() -> {
                System.out.println(Thread.currentThread().getName());
                sleep(200);
            });
        }
        cached.shutdown();

        // 3. ScheduledThreadPool
        System.out.println("=== ScheduledThreadPool ===");
        ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);
        scheduled.scheduleAtFixedRate(
            () -> System.out.println(Thread.currentThread().getName() + " 心跳"),
            0, 1, TimeUnit.SECONDS);
        Thread.sleep(3000);
        scheduled.shutdown();

        // 4. SingleThreadExecutor
        System.out.println("=== SingleThreadExecutor ===");
        ExecutorService single = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 5; i++) {
            final int id = i;
            single.execute(() -> {
                System.out.println(id + " - " + Thread.currentThread().getName());
            });
        }
        single.shutdown();
    }

    private static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { }
    }
}

六、为什么不推荐 Executors?

问题 说明
FixedThreadPool/SingleThreadExecutor 使用无界 LinkedBlockingQueue,任务堆积可撑爆内存
CachedThreadPool/ScheduledThreadPool 最大线程数 Integer.MAX_VALUE,高并发可创建海量线程撑爆内存
没有明确的参数配置 无法设置拒绝策略、队列容量、线程名称等
可读性差 团队成员需要查看源码才能知道具体配置

推荐的创建方式

// 推荐:通过 ThreadPoolExecutor 自定义
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2,                                // corePoolSize
    4,                                // maximumPoolSize
    60L,                              // keepAliveTime
    TimeUnit.SECONDS,                 // unit
    new ArrayBlockingQueue<>(1000),   // 有界队列!
    new ThreadFactoryBuilder()        // 命名线程
        .setNameFormat("业务线程-%d")
        .build(),
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);

七、工作流程比较

flowchart LR
    subgraph FixedThreadPool
        A1["提交任务"] --> B1["核心线程(3)"]
        B1 -->|"核心全忙"| C1["无界队列
无限堆积"
] end subgraph CachedThreadPool A2["提交任务"] --> B2["核心线程(0)"] B2 -->|"SynchronousQueue"| C2["立即创建新线程"] C2 -->|"空闲60s"| D2["回收"] end subgraph SingleThreadExecutor A3["提交任务"] --> B3["单线程(1)"] B3 -->|"有任务"| C3["按顺序处理"] B3 -->|"线程异常退出"| D3["创建新线程替换"] end

八、面试常见问题

Q1:为什么阿里规约禁止使用 Executors 创建线程池?
主要是资源耗尽的风险:FixedThreadPool / SingleThreadExecutor 使用无界队列可能 OOM;CachedThreadPool 最大线程数无上限可能创建海量线程 OOM。建议使用 ThreadPoolExecutor 并指定有界队列明确的拒绝策略

Q2:newSingleThreadExecutor 和 newFixedThreadPool(1) 的区别?
前者的线程池配置无法被外部修改(内部包装为 DelegatedExecutorService),保证了单线程语义;后者可以通过强制类型转换修改 corePoolSize。

Q3:ScheduledThreadPool 如何实现定时任务?
底层使用 DelayedWorkQueue(延迟队列),任务被包装为 ScheduledFutureTask,根据延迟时间排序。线程从队列中取任务时,只有最早到期的任务才能被取出。

Q4:CachedThreadPool 适合什么场景?
适合执行时间短、数量大的异步任务。例如:短连接请求处理、轻量级事件分发。不适合长时间运行的任务(如 Socket 监听)。

Q5:线程池的预定义类型不够用时怎么办?
直接使用 ThreadPoolExecutor 自定义,结合业务场景合理配置 7 大参数,并配合有界队列和适当的拒绝策略。也可继承 ThreadPoolExecutor 重写 beforeExecute/afterExecute 添加监控。

>

线程池 ThreadPoolExecutor 源码级拆解——7 大参数与最佳实践

一、引言:线程池——Java 并发编程的基石

Java 并发编程中,线程池是使用最频繁、踩坑最深的组件之一。无论是 Web 服务器处理 HTTP 请求、RPC 框架处理远程调用,还是批处理任务调度,线程池都是承上启下的关键设施。

使用线程池的好处显而易见:降低资源开销(复用线程而非频繁创建销毁)、提高响应速度(任务提交后无需等待线程创建)、管理线程生命周期(统一的创建、销毁、监控)。但在实际项目中,因为线程池参数配置不当导致的生产事故比比皆是——OOM、CPU 打满、服务雪崩、死锁……这些问题的根源往往在于开发者没有真正理解 ThreadPoolExecutor 内部到底发生了什么。

本文不会停留在”阿里巴巴 Java 开发手册建议用 ThreadPoolExecutor 而非 Executors”这种表层建议上,而是深入 JDK 源码,逐层拆解 ThreadPoolExecutor 的 7 大参数如何协同工作,再给出经过实战检验的最佳实践。

1.1 Executors 的陷阱

// 反面教材:使用 Executors 创建线程池
// 这行代码在很多项目中赫然存在
ExecutorService executor = Executors.newFixedThreadPool(10);

// Executors.newFixedThreadPool 内部实现:
// return new ThreadPoolExecutor(10, 10,
//                               0L, TimeUnit.MILLISECONDS,
//                               new LinkedBlockingQueue());
// LinkedBlockingQueue 的默认容量是 Integer.MAX_VALUE(21亿+)
// 当任务提交速度超过处理速度时,队列无限制堆积 → OOM

// 同样的陷阱存在于:
// Executors.newSingleThreadExecutor()  // 也是无界队列
// Executors.newCachedThreadPool()      // 最大线程数 Integer.MAX_VALUE

这正是阿里巴巴规范背后的深层原因——不是 ThreadPoolExecutor 有问题,而是 Executors 的默认参数隐藏了巨大风险。理解每个参数的含义,才能针对业务场景做出合理配置。

二、ThreadPoolExecutor 的 7 大参数源码拆解

2.1 参数全景

public ThreadPoolExecutor(int corePoolSize,         // 核心线程数
                          int maximumPoolSize,      // 最大线程数
                          long keepAliveTime,       // 空闲线程存活时间
                          TimeUnit unit,            // 存活时间单位
                          BlockingQueue<Runnable> workQueue,  // 任务队列
                          ThreadFactory threadFactory,        // 线程工厂
                          RejectedExecutionHandler handler)   // 拒绝策略
flowchart TD
    subgraph 线程池生命周期
        S[提交任务] --> A{工作线程数 < corePoolSize?}
        A -->|| B[创建核心线程执行任务]
        A -->|| C[任务进入 workQueue]
        C --> D{队列已满?}
        D -->|| E[任务在队列中等待]
        D -->|| F{工作线程数 < maximumPoolSize?}
        F -->|| G[创建非核心线程执行任务]
        F -->|| H[执行拒绝策略]
    end

    B --> I[线程执行任务后]
    I --> J{队列中还有任务?}
    J -->|| K[从队列取任务继续执行]
    J -->|| L{当前线程数 > corePoolSize?}
    L -->|| M[等待 keepAliveTime 后销毁]
    L -->|| N[阻塞等待新任务]

七大参数的协调关系: 上述流程图呈现了 ThreadPoolExecutor 最核心的任务提交-执行-回收流水线。每个参数都在这个流程中扮演特定角色。下面逐一拆解。

2.2 corePoolSize——核心线程数

核心线程的”核”在于:即使空闲也不会被回收(除非设置了 allowCoreThreadTimeOut(true))。

// ThreadPoolExecutor 源码中关于核心线程的关键逻辑
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();

    int c = ctl.get();

    // 第一步:如果工作线程数 < 核心线程数,新建线程
    // 注意:即使其他核心线程空闲,也会新建线程
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))  // true = core
            return;
        c = ctl.get();
    }

    // 第二步:尝试入队
    if (isRunning(c) && workQueue.offer(command)) {
        // ... 入队成功后的双重检查
    }
    // 第三步:尝试创建非核心线程
    else if (!addWorker(command, false))  // false = non-core
        reject(command);  // 拒绝策略
}

最佳实践:核心线程数的估算

// CPU 密集型任务:核心线程数 ≈ CPU 核数 + 1
// IO 密集型任务:核心线程数 ≈ CPU 核数 * (1 + 等待时间/计算时间)
// 
// 网上广为流传的公式:N = Ncpu * Ucpu * (1 + W/C)
// 其中 W 是等待时间,C 是计算时间,Ucpu 是 CPU 利用率目标
// 
// 但是——这个公式在实践中容易让人产生"精确"的错觉。
// IO 等待时间 W 很难精确测量,因为涉及网络延迟、锁竞争等动态因素。

// 更好的做法:进行压测
// 1. 从较小的核心线程数开始(如 CPU 核数 * 2)
// 2. 逐步增大并监控吞吐量、平均响应时间、CPU 利用率
// 3. 找到拐点——即再增大线程数吞吐量不再提升的点

2.3 maximumPoolSize——最大线程数

最大线程数限定了线程池中最多可以同时存在的线程数量,包括核心线程和非核心线程。

// 关键源码:addWorker 方法中的数量检查
private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // ... 状态检查省略 ...

        for (;;) {
            int wc = workerCountOf(c);

            // 核心检查:是否超过容量上限 CAPACITY 或对应的核心/最大限制
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;  // 无法创建新线程
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // 重读
            if (runStateOf(c) != rs)
                continue retry;
        }
    }
    // ... 实际 Worker 创建 ...
}

corePoolSize vs maximumPoolSize 的协作:

flowchart LR
    subgraph 资源弹性伸缩
        CORE["corePoolSize\n⌄ 固定驻留线程数"]
        MAX["maximumPoolSize\n⌄ 峰值允许的最大线程数"]
        QUEUE["workQueue\n⌄ 缓冲任务"]
    end

    CORE -->|"任务激增→队列满→"| QUEUE
    QUEUE -->|"队列满→触发扩容"| MAX
    MAX -->|"任务回稳→空闲线程超时回收"| CORE

反面教材:maxPoolSize 设得过大

// 错误配置
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    10,                              // corePoolSize
    10000,                           // maximumPoolSize(太大!)
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000)  // 有界队列
);

// 问题:当 10000 个线程同时运行时,光是线程栈就占用
// 默认栈大小 1MB × 10000 ≈ 10GB 内存
// 加上线程切换的 CPU 开销,系统瞬间崩溃

最佳实践:maximumPoolSize 应该 > corePoolSize 但有限度

// 合理配置
int cpuCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    cpuCores * 2,                    // corePoolSize
    cpuCores * 4,                    // maximumPoolSize(上限控制)
    30L, TimeUnit.SECONDS,           // 存活时间
    new ArrayBlockingQueue<>(500),   // 有界队列
    new NamedThreadFactory("worker"),
    new CallerRunsPolicy()           // 拒绝策略
);

2.4 keepAliveTime——空闲线程存活时间

非核心线程在空闲了 keepAliveTime 后会被自动回收。这是线程池实现资源弹性的关键机制。

// Worker 线程的 run 方法循环
// getTask() 中使用 keepAliveTime 控制等待时间
final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock();  // 允许中断
    boolean completedAbruptly = true;
    try {
        // 核心循环:不断取任务执行
        while (task != null || (task = getTask()) != null) {
            // ... 执行任务 ...
        }
        completedAbruptly = false;
    } finally {
        // 线程退出时的清理
        processWorkerExit(w, completedAbruptly);
    }
}

private Runnable getTask() {
    boolean timedOut = false;

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // ... 状态检查 ...

        int wc = workerCountOf(c);

        // 关键判断:是否需要"计时等待"
        // timed = true 表示线程需要计时回收
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;  // 返回 null 导致 Worker 退出循环 → 线程销毁
            continue;
        }

        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();  // 核心线程阻塞等待
            if (r != null)
                return r;
            timedOut = true;  // poll 超时
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

这段源码揭示了几个重要结论:

场景 workerCount timed 行为
只有核心线程 wc <= corePoolSize false 核心线程调用 take() 阻塞等待,永不超时
有非核心线程 wc > corePoolSize true 非核心线程调用 poll(keepAliveTime) 等待
允许核心线程超时 allowCoreThreadTimeOut=true true 所有线程都使用计时等待

allowCoreThreadTimeOut 的两个极端:

// 默认:核心线程永远存活
// 适合:长期运行的服务器,需要稳定的线程池
executor.allowCoreThreadTimeOut(false);

// 启用核心线程超时
// 适合:任务波峰波谷明显的场景
// 如:业务低峰期只保留 0 个线程
executor.allowCoreThreadTimeOut(true);
executor.setKeepAliveTime(30, TimeUnit.SECONDS);

2.5 workQueue——任务队列

workQueue 是线程池中最容易被低估的参数。它决定了当核心线程都在忙时,任务如何缓冲

public interface BlockingQueue<E> extends Queue<E> {
    // 入队方法对比:
    boolean add(E e);     // 满了就抛异常 IllegalStateException
    boolean offer(E e);   // 满了就返回 false(不阻塞)
    void put(E e);       // 满了就阻塞等待
    boolean offer(E e, long timeout, TimeUnit unit); // 有限时间阻塞

    // 出队方法对比:
    E take();            // 队列空就阻塞等待
    E poll(long timeout, TimeUnit unit); // 有限时间等待
}

常用队列对比分析:

队列 容量 特点 适用场景 隐患
LinkedBlockingQueue 默认无界 链表结构,入队出队独立锁 任务相对均匀 队列堆积 → OOM
ArrayBlockingQueue 有界 数组结构,单锁 最常用(推荐) 需要合理设定容量
SynchronousQueue 0 不存任务,直接转交给线程 CachedThreadPool 线程创建失控
PriorityBlockingQueue 无界 按优先级出队 任务有优先级需求 同样 OOM
DelayedWorkQueue 无界 定时延迟 ScheduledThreadPool 用到定时场景
LinkedTransferQueue 无界 支持 CAS 操作 高吞吐 同样 OOM
// 核心逻辑:线程池如何与队列交互
public void execute(Runnable command) {
    // ...
    // 当工作线程达到 corePoolSize 后,尝试入队
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 双重检查:入队后如果线程池被关闭,从队列移除并执行拒绝策略
        if (!isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);  // 没有活跃线程也得加一个
    }
    // 入队失败(队列满)→ 尝试创建非核心线程
    else if (!addWorker(command, false))
        reject(command);
}

最佳实践:合理选择队列类型

// ✅ 推荐:ArrayBlockingQueue + 合理容量
int queueSize = 500;  // 根据任务处理能力和延迟要求决定
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(queueSize);

// 估算队列容量的方法:
// 队列容量 ≈ 平均任务处理时长(ms) × 期望 QPS × 安全系数
// 假设处理耗时 20ms,期望 QPS 2000,安全系数 2
// queueSize ≈ 20/1000 × 2000 × 2 = 80
// 取整:100

2.6 ThreadFactory——线程工厂

ThreadFactory 负责创建新线程。它的默认实现很简单——直接 new Thread(),但带来的问题是:

// 默认 ThreadFactory 的问题:
// 1. 线程名称无意义(如 "pool-1-thread-1")
// 2. 非守护线程,可能导致 JVM 无法退出
// 3. 无异常处理
// 4. 默认优先级(NORM_PRIORITY)

// ThreadPoolExecutor 中的默认实现:
static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
        namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-";
    }

    public Thread newThread(Runnable r) {
        return new Thread(r, namePrefix + threadNumber.getAndIncrement());
    }
}

最佳实践:自定义 ThreadFactory

public class NamedThreadFactory implements ThreadFactory {
    private final String namePrefix;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final boolean daemon;
    private final Thread.UncaughtExceptionHandler handler;

    public NamedThreadFactory(String name) {
        this(name, false, null);
    }

    public NamedThreadFactory(String name, boolean daemon, 
                              Thread.UncaughtExceptionHandler handler) {
        this.namePrefix = name + "-";
        this.daemon = daemon;
        this.handler = handler;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, namePrefix + threadNumber.getAndIncrement());
        t.setDaemon(daemon);
        if (handler != null) {
            t.setUncaughtExceptionHandler(handler);
        }
        return t;
    }
}

// 使用
executor = new ThreadPoolExecutor(
    10, 20, 30L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),
    new NamedThreadFactory("biz-worker", false, 
        (t, e) -> log.error("Thread {} died", t.getName(), e)),
    new CallerRunsPolicy()
);

2.7 RejectedExecutionHandler——拒绝策略

当线程池已满(达到 maximumPoolSize 且队列已满)时,拒绝策略决定如何处理新提交的任务。

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

JDK 内置 4 种拒绝策略:

flowchart TD
    S[队列满 + 线程满] --> R{选择哪种拒绝策略?}
    R --> A[AbortPolicy\n抛出 RejectedExecutionException]
    R --> B[CallerRunsPolicy\n在提交者线程中执行]
    R --> C[DiscardPolicy\n静默丢弃]
    R --> D[DiscardOldestPolicy\n丢弃队列中最旧的任务]

    A -->|优点: 明确定义| A1[缺点: 可能导致\n调用方异常]
    B -->|优点: 背压机制| B1[缺点: 提交者线程\n执行时间增长]
    C -->|优点: 不抛异常| C1[缺点: 任务丢了下\n都不知道]
    D -->|优点: 丢弃最老的| D1[缺点: 优先级高的\n可能被丢弃]
// 四种内置策略的源码核心逻辑:

// 1. AbortPolicy(默认)
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    throw new RejectedExecutionException("Task " + r.toString() + 
        " rejected from " + e.toString());
}

// 2. CallerRunsPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        r.run();  // 在提交者的线程中同步执行!
    }
}

// 3. DiscardPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    // 什么都不做——静默丢弃
}

// 4. DiscardOldestPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        e.getQueue().poll();  // 丢弃队列中最老的任务
        e.execute(r);         // 重新提交新任务
    }
}

最佳实践:生产环境推荐策略

// 方案 1: CallerRunsPolicy —— 隐式背压
// 适用于:对任务丢失零容忍的场景
// 效果:提交者线程执行任务 → 提交速度自然减慢 → 系统自动调节

// 方案 2: 自定义策略 —— 最灵活
public class RetryRejectedPolicy implements RejectedExecutionHandler {
    private static final int MAX_RETRIES = 3;
    private static final long RETRY_INTERVAL_MS = 100;

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            // 记录告警
            log.warn("Task rejected, queue={}, active={}, pool={}",
                     e.getQueue().size(), e.getActiveCount(), e.getPoolSize());
            // 尝试重新提交(可以放到 MQ/其他方式处理)
            RetryTask task = new RetryTask(r, MAX_RETRIES);
            scheduledExecutor.schedule(() -> {
                if (!e.isShutdown()) {
                    e.execute(task);
                }
            }, RETRY_INTERVAL_MS, TimeUnit.MILLISECONDS);
        }
    }
}

三、ctl——线程池的核心状态管理

ThreadPoolExecutor 用一个 AtomicIntegerctl)同时管理两个状态:线程池运行状态(高 3 位)和工作线程数量(低 29 位):

// 包级私有——核心控制状态
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

// COUNT_BITS = 29 (Integer.SIZE - 3)
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;  // 000 11111...111

// 运行状态存储在高 3 位
// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;  // 111
private static final int SHUTDOWN   =  0 << COUNT_BITS;  // 000
private static final int STOP       =  1 << COUNT_BITS;  // 001
private static final int TIDYING    =  2 << COUNT_BITS;  // 010
private static final int TERMINATED =  3 << COUNT_BITS;  // 011

// 状态变化流程
private static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

// 线程池状态变迁
// RUNNING → SHUTDOWN: 调用 shutdown(),不接受新任务
// RUNNING → STOP:     调用 shutdownNow(),中断所有线程
// SHUTDOWN → TIDYING: 队列为空且工作线程为 0
// STOP → TIDYING:      工作线程为 0
// TIDYING → TERMINATED: terminated() 钩子方法执行完毕
flowchart LR
    RUNNING -->|shutdown| SHUTDOWN
    RUNNING -->|shutdownNow| STOP
    SHUTDOWN -->|队列空且线程数=0| TIDYING
    STOP -->|线程数=0| TIDYING
    TIDYING -->|执行 terminated| TERMINATED

这种用单个 AtomicInteger 同时管理两个状态的技巧,使得 ThreadPoolExecutor 在判断”当前状态是否允许创建新线程”时只需要一次 CAS 操作,无需加锁。

四、Worker——线程池中的线程包装

每个真正执行任务的线程都被包装为一个 Worker

private final class Worker extends AbstractQueuedSynchronizer
    implements Runnable {

    final Thread thread;  // 实际执行任务的线程
    Runnable firstTask;   // 创建时指定的第一个任务(可能为 null)
    volatile long completedTasks;  // 已完成任务数

    Worker(Runnable firstTask) {
        setState(-1);  // 禁止中断直到 runWorker 开始
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }

    public void run() {
        runWorker(this);  // 委托给外部方法
    }

    // 继承 AQS 实现简单的互斥锁
    // 用于控制线程中断时的竞争条件
    // 0 表示未锁定,1 表示已锁定
    protected boolean tryAcquire(int unused) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }
}

Worker 继承 AQS 而不是 ReentrantLock,是为了实现不可重入的独占锁——这在中断线程时非常重要,确保不会在持有锁时被中断。

五、线程池监控——你的池子在干活吗?

5.1 内置监控指标

ThreadPoolExecutor 提供了多个可访问的监控指标:

public class ThreadPoolMonitor {

    public static void printMetrics(ThreadPoolExecutor executor, String name) {
        // 基础指标
        int poolSize = executor.getPoolSize();            // 当前线程数
        int activeCount = executor.getActiveCount();      // 活跃线程数
        int corePoolSize = executor.getCorePoolSize();    // 核心线程数
        int maximumPoolSize = executor.getMaximumPoolSize();
        long completedTaskCount = executor.getCompletedTaskCount();  // 完成数量
        long taskCount = executor.getTaskCount();         // 总任务数
        int queueSize = executor.getQueue().size();       // 队列堆积数
        int remainingCapacity = executor.getQueue().remainingCapacity();  // 剩余容量

        log.info("[{}] PoolSize={}, Active={}, Completed={}, Queue={}/{}, Core/Max={}/{}",
                 name, poolSize, activeCount, completedTaskCount,
                 queueSize, queueSize + remainingCapacity,
                 corePoolSize, maximumPoolSize);
    }
}

最佳实践:线程池监控的黄金指标

指标 正常范围 告警阈值 含义
activeCount < corePoolSize > corePoolSize * 0.8 核心线程可能不够
queueSize 接近 0 > queueCapacity * 0.7 任务处理能力不足
completedTaskCount 持续增长 长时间不增长 线程池阻塞或死锁
poolSize = corePoolSize > corePoolSize 触发了临时扩容
rejectedCount 0 > 0 系统过载
longestTaskTime < 1s > 10s 有任务长时间阻塞

5.2 动态调整线程池参数

ThreadPoolExecutor 支持运行时动态调整参数——这是很多人不知道但非常实用的特性:

public class DynamicThreadPool {
    private final ThreadPoolExecutor executor;

    public DynamicThreadPool() {
        this.executor = new ThreadPoolExecutor(
            10, 20, 30L, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100),
            new NamedThreadFactory("dynamic"),
            new CallerRunsPolicy()
        );

        // 启动调整线程
        scheduleAdjustment();
    }

    // 根据监控数据动态调整
    public void adjustPoolSize(int newCore, int newMax) {
        executor.setCorePoolSize(newCore);
        executor.setMaximumPoolSize(newMax);
    }

    // 自动调节:根据队列深度调整
    private void scheduleAdjustment() {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        scheduler.scheduleAtFixedRate(() -> {
            int queueSize = executor.getQueue().size();
            int activeCount = executor.getActiveCount();

            if (queueSize > 100 && activeCount >= executor.getCorePoolSize()) {
                // 队列堆积,需要扩容
                executor.setCorePoolSize(executor.getCorePoolSize() + 2);
                executor.setMaximumPoolSize(executor.getMaximumPoolSize() + 2);
                log.info("Thread pool expanded: core={}, max={}",
                         executor.getCorePoolSize(), executor.getMaximumPoolSize());
            } else if (queueSize == 0 && executor.getPoolSize() > 10) {
                // 任务很少,缩容
                int newCore = Math.max(4, executor.getCorePoolSize() - 1);
                executor.setCorePoolSize(newCore);
                log.info("Thread pool shrunk: core={}", newCore);
            }
        }, 10, 30, TimeUnit.SECONDS);
    }
}

六、常见线程池陷阱与实战案例

6.1 陷阱:线程池中的异常吞没

// ❌ 反例:线程池中的异常被吞没
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> {
    // 这个异常不会传播出来
    throw new RuntimeException("Something went wrong!");
});
// submit() 的异常被包装在 Future 中
// 如果不调用 Future.get(),异常永远不会被知道!

// ✅ 正例 1:使用 execute() 而不是 submit()
executor.execute(() -> {
    throw new RuntimeException("This will be caught by handler!");
});

// ✅ 正例 2:捕获异常并记录
executor.execute(() -> {
    try {
        doRiskyWork();
    } catch (Exception e) {
        log.error("Task failed", e);
        // 自定义处理
    }
});

// ✅ 正例 3:设置 UncaughtExceptionHandler
ThreadFactory factory = r -> {
    Thread t = new Thread(r);
    t.setUncaughtExceptionHandler((thread, e) -> 
        log.error("Thread {} died with exception", thread.getName(), e));
    return t;
};

6.2 陷阱:任务依赖导致饥饿死锁

// ❌ 反例:单线程池中的任务依赖
ExecutorService executor = new ThreadPoolExecutor(
    1, 1, 0L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>()
);

// 任务 A 提交任务 B
Future<String> future = executor.submit(() -> {
    // 任务 A
    log.info("Task A started");
    // 等待任务 B 的结果
    Future<String> bResult = executor.submit(() -> "B");  // 🔴 死锁!
    return "A" + bResult.get();  // 永远等不到,因为线程池只有一个线程
});

// 执行结果:死锁!线程池中唯一的工作线程被任务 A 占用
// 任务 B 永远无法获得线程资源

解决方案: 识别依赖关系,使用不同的线程池,或者使用 CompletableFuture 避免阻塞等待。

6.3 陷阱:线程池关闭不当

// ❌ 反例:直接 shutdownNow() 导致任务丢失
executor.shutdownNow();  // 中断所有线程,返回未执行的任务列表
// 返回值中的任务如果不处理,就永远丢失了

// ✅ 正例:优雅关闭
public void gracefulShutdown(ExecutorService executor, long timeout, TimeUnit unit) {
    executor.shutdown();  // 不再接受新任务,等待已有任务完成
    try {
        if (!executor.awaitTermination(timeout, unit)) {
            executor.shutdownNow();  // 超时后强制关闭
            if (!executor.awaitTermination(timeout, unit)) {
                log.error("Executor did not terminate");
            }
        }
    } catch (InterruptedException e) {
        executor.shutdownNow();
        Thread.currentThread().interrupt();
    }
}

6.4 陷阱:ThreadLocal 与线程复用

// ❌ 反例:线程池中使用 ThreadLocal 不清理
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
    final int value = i;
    executor.execute(() -> {
        threadLocal.set(value);
        doWork();
        // 这里没有 remove()!
        // 下一个使用这个线程的任务会读到错误的值
    });
}

// ✅ 正例:用完立即清理
executor.execute(() -> {
    try {
        threadLocal.set(value);
        doWork();
    } finally {
        threadLocal.remove();  // 必须清理!
    }
});

七、总结

  1. ThreadPoolExecutor 的 7 大参数是一个有机整体。 corePoolSize 决定基础并发量,workQueue 承载缓冲,maximumPoolSize 控制峰值弹性,keepAliveTime 管理资源释放,ThreadFactory 提供线程信息,RejectedExecutionHandler 处理过载。任何参数的错误配置都可能引发严重问题。

  2. 活用有界队列 + CallerRunsPolicy。 这组组合是生产环境最推荐的配置。有界队列防止 OOM,CallerRunsPolicy 提供隐式背压机制。

  3. 理解 ctl 的设计哲学。 一个 AtomicInteger 同时管理状态和线程数,用位运算实现无锁的状态判断,是 JDK 源码中”用复杂技巧解决简单问题”的精妙案例。

  4. 善用监控。 线程池的黑盒特性决定了必须持续监控活跃线程数、队列深度、拒绝次数等指标。这些指标是定位性能问题的最直接线索。

  5. 警惕线程池陷阱。 异常吞没、任务依赖死锁、ThreadLocal 污染、关闭不当——这些是生产环境最常遇到的问题,需要在代码中预设保护机制。

  6. 动态调整让你的线程池”活起来”。 ThreadPoolExecutor 提供了运行时调整参数的接口,结合监控数据可以实现自动伸缩。


Java 内存模型(JMM)与 volatile——从 CPU 到代码的全链路理解

一、引言:从 CPU 到 Java——为什么需要内存模型?

写 Java 并发代码时,”多线程下变量不可见”、”指令重排导致奇怪的结果”这类问题困扰着每一个开发者。本质上,这些问题都源于同一个根源:CPU、编译器、内存三者之间有一个巨大的性能差距

CPU 的主频在过去几十年里从几十 MHz 提升到了 5GHz+,而内存的访问延迟却只从 60ns 降到了约 100ns——CPU 比内存快了至少两个数量级。为了填补这个鸿沟,现代 CPU 和编译器引入了一系列优化:缓存、指令流水线、乱序执行、分支预测。这些优化在多线程环境下,却成了问题的根源——不同 CPU 核心看到的内存视图可能不一致。

这就是 Java 内存模型(Java Memory Model, JMM)要解决的问题:定义一套规则,让我们能写出正确的并发程序,同时不牺牲太多性能。

flowchart TD
    subgraph 问题链
        A[CPU 与内存的速度差距] --> B[引入 CPU Cache]
        B --> C[缓存一致性协议\n如 MESI]
        C --> D[Store Buffer / \nInvalidation Queue]
        D --> E[指令重排\nCPU 和编译器]
        E --> F[可见性问题\n有序性问题]
    end

    subgraph 解决方案
        G[Java Memory Model]
        H[JMM 规范]
        I[volatile / synchronized\nfinal / happens-before]
    end

    F --> G
    G --> H
    H --> I

1.1 从 CPU 到 JMM——层级关系

硬件层面:CPU → 多级缓存 → 内存 → 磁盘
JVM 层面:线程 → 工作内存(线程栈)→ 主内存 → 持久层

flowchart LR
    subgraph 线程 A
        A1[Thread A\n工作内存] -->|read/load| A2[本地缓存]
    end
    subgraph 线程 B
        B1[Thread B\n工作内存] -->|read/load| B2[本地缓存]
    end
    subgraph 主内存
        M1[Heap / Method Area\n共享变量]
    end

    A2 <-->|store/write| M1
    B2 <-->|store/write| M1

    style A2 fill:#3498db,color:#fff
    style B2 fill:#e74c3c,color:#fff
    style M1 fill:#27ae60,color:#fff

JMM 定义了 8 种内存操作及其执行规则:

操作 作用方 目标 说明
lock 线程 主内存 锁定变量(仅一个线程能锁)
unlock 线程 主内存 解锁变量
read 线程 主内存 将主内存的变量读到工作内存
load 线程 工作内存 将 read 得到的值放入本地变量副本
use 线程 工作内存 把工作内存变量值传递给执行引擎
assign 线程 工作内存 将执行引擎的结果赋值给工作内存变量
store 线程 工作内存 将工作内存的变量传递到主内存
write 线程 主内存 将 store 的值写入主内存变量

二、指令重排——看不见的”乱序”

2.1 重排的来源

指令重排有三个层面:

flowchart LR
    subgraph 编译器重排
        COD[源代码] -->|词法/语法分析| AST
        AST -->|JIT 编译器优化| IR[中间代码]
        IR -->|指令调度| ASM[汇编代码]
    end

    subgraph CPU 重排
        ASM -->|取指| IF[Instruction Fetch]
        IF -->|乱序执行| OOE[Out-of-Order Execution]
        OOE -->|提交| RET[Retire / Commit]
    end

    subgraph 内存系统重排
        RET -->|Store Buffer| SB
        SB -->|MESI 协议| CACHE[CPU Cache]
        CACHE -->|写入| MEM[Main Memory]
    end

编译器和 CPU 为什么要重排? 考虑以下代码:

// 原始代码
int a = 1;        // A
int b = 2;        // B
int c = a + b;    // C

从数据依赖来看,C 依赖 A 和 B,但 A 和 B 之间没有依赖关系。CPU 可以:

乱序执行: A → B → C
      或: B → A → C

两者的结果完全一致,但 B 先执行可能让 CPU 的数据缓存命中率更高(如果 b 已经在缓存中的话)。

2.2 重排如何导致并发问题

// 经典例子:双检锁(DCL)
public class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {                    // 检查 1
            synchronized (Singleton.class) {
                if (instance == null) {            // 检查 2
                    instance = new Singleton();    // 问题就在这里!
                }
            }
        }
        return instance;
    }
}

instance = new Singleton() 在字节码层面分为三步:

// 1. memory = allocate();      // 分配内存空间
// 2. ctorInit(memory);         // 调用构造函数初始化
// 3. instance = memory;        // 将引用指向内存

// 如果 2 和 3 被重排为:
// 1. memory = allocate();
// 2. instance = memory;        // 先赋值(此时对象未初始化!)
// 3. ctorInit(memory);         // 后初始化

// 另一个线程在 2 和 3 之间执行检查 1:
// if (instance == null) → false(instance 非 null)
// return instance; → 拿到未初始化的对象!

解决方案: 使用 volatile 禁止重排:

private static volatile Singleton instance;  // volatile 禁止指令重排

2.3 什么是”as-if-serial”语义?

JMM 允许编译器和 CPU 在不改变单线程执行结果的前提下任意重排指令。这个原则叫做 as-if-serial:

// as-if-serial 允许的重排
int a = 1;     // A
int b = 2;     // B ← A 和 B 没有数据依赖,可以重排

// as-if-serial 不允许的重排
int a = 1;     // A
int b = a + 1; // B ← B 依赖 A,禁止重排
int c = b + 1; // C ← C 依赖 B,禁止重排
// 最终执行顺序只能是 A → B → C

这个原则本身是合理的,但在多线程环境下,单个线程的 as-if-serial 无法保证全局的一致性。

三、volatile——最轻量的同步机制

3.1 volatile 的两层语义

public class VolatileDemo {

    // volatile 保证:
    // 1. 可见性:对 volatile 变量的写,会立即被其他线程看到
    // 2. 有序性:禁止 JIT 编译器和 CPU 对 volatile 相关的指令重排
    private volatile boolean flag = false;
    private int data = 0;

    // 线程 A 执行
    public void writer() {
        data = 42;          // 普通写
        flag = true;        // volatile 写(禁止与前面的普通写重排)
    }

    // 线程 B 执行
    public void reader() {
        if (flag) {         // volatile 读
            System.out.println(data);  // 保证看到 42
        }
    }
}

volatile 的可见性如何保证? 在 x86 架构下:

// 对 volatile 变量的写操作,编译器会在生成的汇编中插入一条
// lock 前缀指令(或者内存屏障指令)
//
// lock addl $0, (%rsp)
//
// 这条指令的作用:
// 1. 将当前 CPU 的 Store Buffer 全部写入缓存
// 2. 使其他 CPU 中对应的缓存行失效
// 3. 相当于一个"全屏障"(StoreLoad Barrier)

3.2 volatile 的内存屏障实现

JVM 在 volatile 操作周围插入内存屏障(Memory Barrier)

flowchart LR
    subgraph volatile 
        W1[普通写] --> WB[StoreStore Barrier ]
        WB --> W2[volatile ]
        W2 --> WB2[StoreLoad Barrier ]
    end

    subgraph volatile 
        VB[LoadLoad Barrier ] --> V1[volatile ]
        V1 --> VB2[LoadStore Barrier ]
        VB2 --> V2[普通读]
    end

JMM 中的四种内存屏障:

屏障类型 指令组合 作用
LoadLoad Load1 → LoadLoad → Load2 禁止 Load1 和后续 Load2 的重排
StoreStore Store1 → StoreStore → Store2 禁止 Store1 和后续 Store2 的重排
LoadStore Load1 → LoadStore → Store2 禁止 Load1 和后续 Store2 的重排
StoreLoad Store1 → StoreLoad → Load2 全屏障,所有屏障中最重的

注意: StoreLoad 屏障的开销最大,因为它需要清空 Store Buffer 并等待写完成。volatile 写操作在 x86 上插入的就是 StoreLoad 屏障。

3.3 volatile 不能保证原子性

// ❌ 反例:volatile 不能保证复合操作的原子性
public class VolatileCounter {
    private volatile int count = 0;

    public void increment() {
        count++;  // 这行代码不是原子的!
    }
    // count++ 在字节码层面:
    // 1. GETFIELD count      // read
    // 2. ICONST_1            // 常量 1
    // 3. IADD                // add
    // 4. PUTFIELD count      // write
    // 虽然每一步都是原子的,但整体不是!
    // 可能出现线程 A 读到 5,线程 B 也读到 5
    // A 写回 6,B 也写回 6 → 丢失一次递增
}

// ✅ 正例:使用 AtomicInteger
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
    count.incrementAndGet();  // CAS 保证原子性
}

// ✅ 或者使用 synchronized
private int count = 0;
public synchronized void increment() {
    count++;
}

3.4 volatile 的适用场景

// ✅ 适用场景 1:状态标志
public class ShutdownDemo {
    private volatile boolean shutdown;

    public void shutdown() {
        shutdown = true;  // 工作线程通过读 volatile 值感知关闭
    }

    public void doWork() {
        while (!shutdown) {
            // 处理任务
        }
    }
}

// ✅ 适用场景 2:一次性安全发布(one-time safe publication)
public class SafePublication {
    private volatile Map<String, String> config;

    public void updateConfig(Map<String, String> newConfig) {
        // 对 newConfig 的所有写操作
        // 在 volatile 写之前已经完成
        this.config = new ConcurrentHashMap<>(newConfig);
        // volatile 写确保上述构造函数的结果对其他线程可见
    }

    public String getConfig(String key) {
        Map<String, String> c = config;  // volatile 读
        return c != null ? c.get(key) : null;
    }
}

// ✅ 适用场景 3:观察者模式中的事件通知
private volatile boolean eventFired;

volatile 不适合的场景:
– 需要原子性的复合操作(count++)
– 多个变量间的逻辑约束(如 x==0 时 y==1 这样的不变量)
– 需要显式锁的复杂条件

四、Happens-Before——JMM 的灵魂

4.1 什么是 Happens-Before?

Happens-Before 是 JMM 中最核心的概念。它不是”时间上的先后”,而是一种可见性保证——如果 A happens-before B,那么 A 操作的结果对 B 操作是可见的,且 A 的排序(如果被重排)一定在 B 之前完成。

4.2 JMM 中的 8 条 Happens-Before 规则

flowchart TD
    subgraph Happens-Before 规则
        R1[程序次序规则\n同一线程中, 写在前面的 happens-before 后面的]
        R2[volatile 变量规则\n对一个 volatile 的写, happens-before\n任意后续对这个 volatile 的读]
        R3[锁规则\n对一个锁的 unlock, happens-before\n后续对这个锁的 lock]
        R4[传递性\nA happens-before B, B happens-before C\n A happens-before C]
        R5[线程启动规则\nThread.start() happens-before\n被启动线程的所有操作]
        R6[线程终止规则\n被终止线程的所有操作 happens-before\nThread.join() 返回]
        R7[线程中断规则\nThread.interrupt() happens-before\n被中断线程检测到中断]
        R8[对象终结规则\n对象构造完成 happens-before\nfinalize() 开始]
    end

应用传递性链来分析经典问题:

public class HappensBeforeChain {
    private int x = 0;
    private volatile boolean flag = false;

    public void writer() {
        x = 42;                       // 操作 A(普通写)
        flag = true;                  // 操作 B(volatile 写)
    }

    public void reader() {
        if (flag) {                   // 操作 C(volatile 读)
            int r = x;                // 操作 D(普通读)
            // 根据规则:
            // A happens-before B(程序次序规则)
            // B happens-before C(volatile 规则)
            // C happens-before D(程序次序规则)
            // → A happens-before D(传递性)
            // → r == 42(保证)
            assert r == 42;
        }
    }
}

4.3 锁规则详解

public class LockRule {
    private int x = 0;
    private final Object lock = new Object();

    // 线程 A
    public void lockWriter() {
        synchronized (lock) {     // lock → unlock
            x = 42;
        }                         // unlock
    }

    // 线程 B
    public void lockReader() {
        synchronized (lock) {     // lock(同一个锁)
            int r = x;            // 保证看到 x == 42
        }
    }
}

锁规则的关键:必须是对同一个监视器对象的 unlock → lock。如果线程 A 对 lock1 解锁,线程 B 对 lock2 加锁,则没有 happens-before 关系。

五、final 的内存语义

5.1 final 的多线程保证

// final 字段在构造函数返回之前被正确构造的情况下,
// 其他线程保证能看到 final 字段的正确值
public class FinalExample {
    private final int x;   // final 字段
    private int y;         // 普通字段
    private static FinalExample instance;

    public FinalExample() {
        x = 42;            // final 写
        y = 10;            // 普通写
    }

    public static void writer() {
        instance = new FinalExample();
    }

    public static void reader() {
        FinalExample obj = instance;
        if (obj != null) {
            int r1 = obj.x;  // 保证是 42(final 保证)
            int r2 = obj.y;  // 可能是 0(普通字段无保证)
        }
    }
}

final 的内存语义在 JSR 133 中得到加强:
– 构造函数中对 final 字段的写,与构造函数返回后的引用赋值之间,禁止重排
– 构造函数中修改 final 字段的写,禁止与后续可能把对象引用发布到其他线程的操作一起重排

六、JMM 在实际编码中的应用

6.1 安全发布对象的四种方式

public class SafePublish {
    private Map<String, String> config;

    // 方式 1:通过 volatile 发布
    private volatile List<String> list1;
    public void setList1(List<String> list) {
        this.list1 = Collections.unmodifiableList(new ArrayList<>(list));
    }

    // 方式 2:通过 synchronized 发布
    private List<String> list2;
    public synchronized void setList2(List<String> list) {
        this.list2 = Collections.unmodifiableList(new ArrayList<>(list));
    }
    public synchronized List<String> getList2() {
        return list2;
    }

    // 方式 3:通过 final 发布
    private static class ConfigHolder {
        static final Map<String, String> CONFIG = loadConfig();
    }
    public static Map<String, String> getConfig() {
        return ConfigHolder.CONFIG;
    }

    // 方式 4:通过原子引用发布
    private final AtomicReference<List<String>> listRef = new AtomicReference<>();
    public void setList(List<String> list) {
        listRef.set(Collections.unmodifiableList(new ArrayList<>(list)));
    }
    public List<String> getList() {
        return listRef.get();
    }
}

6.2 JMM 在不同 CPU 架构上的差异

架构 特点 对 JMM 的影响
x86-TSO 相对保守,只有 StoreLoad 是真正的屏障 其他屏障都是 CPU 级 NOP
ARM / PowerPC 极弱内存模型,几乎任意重排 JMM 需要更多屏障指令
RISC-V 可配置,支持 TSO 和 WMO 与 JMM 的兼容性取决于具体实现

反面教材:x86 上跑得好的代码在 ARM 上出问题

// 在 x86 上,普通写不会和普通读重排(x86 保证)
// 但在 ARM 上,下面的代码可能有问题:
public class WeakMemoryIssue {
    private int a, b;
    private int x, y;

    // 线程 A
    public void writeA() { a = 1; x = b; }
    // 线程 B
    public void writeB() { b = 1; y = a; }

    // 在 x86 上,不可能出现 (x==0 && y==0)
    // 在 ARM/PowerPC 上,可能出现!
}

七、JMM 的底层实现

7.1 C++ 内存模型与 JMM 的关系

JVM 是用 C++ 实现的,JMM 最终通过底层的内存屏障原语来实现。HotSpot 在不同的 CPU 架构上使用不同的屏障实现:

// HotSpot 源码(os_cpu 目录下)中的内存屏障实现
// x86 版本:
inline void OrderAccess::loadload()   { compiler_barrier(); }
inline void OrderAccess::storestore() { compiler_barrier(); }
inline void OrderAccess::loadstore()  { compiler_barrier(); }
inline void OrderAccess::storeload()  {
    // 唯一的硬件屏障
    __asm__ volatile ("mfence" : : : "memory");
}

// ARM 版本:
inline void OrderAccess::loadload()   { __asm__ volatile ("dmb ishld" : : : "memory"); }
inline void OrderAccess::storestore() { __asm__ volatile ("dmb ishst" : : : "memory"); }
inline void OrderAccess::loadstore()  { __asm__ volatile ("dmb ish" : : : "memory"); }
inline void OrderAccess::storeload()  { __asm__ volatile ("dmb ish" : : : "memory"); }

为什么 x86 上的 LoadLoad、StoreStore、LoadStore 屏障都是空的? 因为 x86-TSO 内存模型本身就保证了不会对这些操作进行重排。只有在跨写入和读取时(StoreLoad),才需要真正的硬件屏障。

7.2 编译器屏障 vs 硬件屏障

// 编译器屏障:阻止编译器重排
inline void compiler_barrier() {
    __asm__ volatile ("" : : : "memory");  // 阻止编译器优化
}

// 硬件屏障:阻止 CPU 重排
inline void hardware_barrier() {
    __asm__ volatile ("mfence" : : : "memory");  // x86 StoreLoad 屏障
}

区别: 编译器屏障只影响编译器的指令排序,不影响 CPU 的乱序执行。在 x86 上,除了 StoreLoad 不需要硬件屏障的原因是 x86 的 TSO 模型天然保证了其他类型的排序约束。

八、JMM 常见误解与最佳实践

8.1 误解澄清

// 误解 1:volatile 比 synchronized 快
// 事实:在低竞争场景下 volatile 更快
//       在高竞争场景下 volatile 的自旋可能会导致吞吐量下降
//       并不总是比 synchronized 快

// 误解 2:JMM 保证 64 位变量的读写是原子的
// 事实:JMM 保证 volatile long/double 是原子的
//       非 volatile long/double 在 32 位 JVM 上可能不是原子的
//       (JDK 9+ 中已经修复,默认原子访问)

// 误解 3:final 字段初始化后永远不会变
// 事实:通过反射可以修改 final 字段
//       反射修改后的值不能保证可见性

// 误解 4:加了 synchronized 就不用关心 JMM
// 事实:synchronized 确实提供了完整的可见性保证
//       但理解 JMM 有助于写出更高效的并发代码
//       比如知道哪些操作可以移出同步块

8.2 编写安全并发代码的检查清单

检查项 说明
读写共享变量是否同步? volatile 或 synchronized 或原子类
复合操作是否原子? i++ → AtomicInteger; if-then-act → CAS
对象是否安全发布? 检查构造函数中是否 this 逸出
多变量之间有逻辑约束? 需要使用锁确保原子性
是否依赖 platform-specific 行为? x86 上能跑不等于 ARM 上能跑

九、总结

  1. JMM 定义了 Java 并发编程的”宪法”。 它规定了线程之间如何通过共享内存通信,以及什么情况下的重排是可见的。”happens-before”规则是理解 JMM 的钥匙。

  2. volatile 是最轻量的线程同步机制。 它提供了可见性和有序性,但不提供原子性。合理使用 volatile(状态标志、一次性安全发布等场景)可以减少对锁的依赖,提升性能。

  3. 内存屏障是 JMM 的硬件映射。 不同 CPU 架构对 JMM 的支持程度不同。x86 的能力最强(只在 StoreLoad 需要硬件屏障),ARM/PowerPC 需要更多的硬件屏障指令。这也是为什么在 x86 上测试通过的并发代码,迁移到 ARM 服务器上可能出现问题。

  4. final 在 JSR 133 后获得了增强的内存语义。 正确构造的 final 字段可以安全地在多线程间共享,这是不可变对象并发安全的基础保证。

  5. Happens-Before 规则链是分析并发问题的第一步。 在讨论任何并发问题之前,先画出 happens-before 关系链,这是比阅读代码本身更重要的步骤。

  6. 工具辅助分析。 Java 提供了 jcstress(并发压力测试)来验证并发代码的正确性,建议在编写复杂并发逻辑时使用。


从 synchronized 到 AQS——Java 并发锁的进化之路

一、引言:Java 锁的进化史

Java 并发锁的发展史,就是一部从”臃肿缓慢”到”轻量高效”的进化史。JDK 1.0 就提供了 synchronized 关键字,但初期实现极其笨重——它是一个”重量级锁”,直接依赖操作系统的互斥量(mutex),线程阻塞和唤醒都需要内核态切换,在 JDK 1.5 之前一直被诟病为”Java 慢”的罪魁祸首之一。

Java 5 的 java.util.concurrent.locks 包引入了 ReentrantLock 等显式锁,基于 Doug Lea 的 AbstractQueuedSynchronizer(AQS) 框架,为 Java 锁的灵活性和性能打开了新世界。但真正让 Java 锁脱胎换骨的是 Java 6 对 synchronized 的优化——引入偏向锁、轻量级锁、锁粗化、锁消除等机制,把 synchronized 从”穷人锁”变成了”万能锁”。

时至今日,synchronized 和 AQS 之间不再是”谁比谁快”的二元竞争关系,而是各有适用场景的伙伴。本文将从源码层面,完整梳理 Java 并发锁的进化之路,带你理解从 synchronized 到 AQS 的底层原理。

flowchart LR
    subgraph JDK 版本演进
        JDK5[JDK 5] -->|引入| ReentrantLock
        JDK5 -->|引入| AQS
        JDK6[JDK 6] -->|优化| Synchronized
        JDK6 -->|偏向锁| BiasLock
        JDK6 -->|轻量级锁| LightLock
        JDK6 -->|锁消除| LockElimination
        JDK15[JDK 15] -->|默认关闭| BiasLock
        JDK21[JDK 21] -->|虚拟线程| VirtualThread
    end

    Synchronized -->|重量级| OS_Mutex[OS Mutex]
    ReentrantLock --> AQS
    AQS -->|基于| CAS[无锁 CAS]

二、synchronized——从字节码到锁升级

2.1 字节码中的 synchronized

// synchronized 的三种用法
public class SynchronizedDemo {

    // 1. 普通同步方法,锁的是当前实例对象
    public synchronized void instanceMethod() {
        // 方法体
    }

    // 2. 静态同步方法,锁的是当前类的 Class 对象
    public static synchronized void staticMethod() {
        // 方法体
    }

    // 3. 同步代码块,锁的是指定的对象
    public void blockMethod() {
        synchronized (this) {
            // 临界区
        }
    }
}

字节码层面分析:

// 同步方法的字节码特征:
// ACC_SYNCHRONIZED 标志在方法访问标志中
// JVM 根据这个标志判断是否需要获取锁

// 同步代码块的字节码特征:
// monitorenter  — 获取指定对象的监视器锁
// monitorexit   — 释放锁(两个:正常退出 + 异常退出)

// 反编译结果:
// 0: aload_0
// 1: dup
// 2: astore_1
// 3: monitorenter         ← 进入同步块,尝试获取锁
// 4: aload_1
// 5: monitorexit          ← 正常退出,释放锁
// 6: goto 14
// 9: astore_2
// 10: aload_1
// 11: monitorexit         ← 异常退出,保证释放锁
// 12: aload_2
// 13: athrow
// 14: return

关键点: JVM 保证了无论同步代码块是正常退出还是异常退出,都会执行 monitorexit。这也是 synchronized 被称为”内置锁”的原因——你永远不用担心忘记释放锁。

2.2 锁升级——从无锁到重量级

JDK 6 之后,synchronized 引入了锁升级机制,锁的状态从低到高依次为:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。锁只能升级不能降级:

flowchart TD
    N[无锁状态] -->|第一个线程访问| B[偏向锁]
    B -->|另一个线程竞争| L[轻量级锁]
    L -->|自旋失败或\n竞争加剧| H[重量级锁]

    B -->|全局安全点| SB[偏向锁撤销]
    SB --> N

    style B fill:#27ae60,color:#fff
    style L fill:#f39c12,color:#fff
    style H fill:#e74c3c,color:#fff

偏向锁

偏向锁的核心思想是:锁不仅没有竞争,而且一直被同一个线程持有

// Mark Word 结构(32位 JVM)
// 无锁态:    | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 |
// 偏向锁态:   | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 |
// 轻量级锁:   | ptr_to_lock_record:62 | 00 |
// 重量级锁:   | ptr_to_heavyweight_monitor:62 | 10 |

// 偏向锁的获取过程(简化)
// 1. 检查 Mark Word 中偏向锁标志位是否为 1
// 2. 如果是,检查线程 ID 是否指向当前线程
// 3. 如果是,直接进入方法体(CAS 都不需要!)
// 4. 如果不是,通过 CAS 尝试将线程 ID 更新为当前线程
// 5. 如果 CAS 失败,说明有线程竞争,进入偏向锁撤销流程

// 偏向锁撤销流程:
// 1. 等待全局安全点(SafePoint)——所有线程暂停
// 2. 检查持有偏向锁的线程是否还活着
//    - 如果已结束:将对象头恢复为无锁态
//    - 如果还在执行:升级为轻量级锁或重量级锁
// 3. 恢复执行

偏向锁在 JDK 15 被默认关闭,原因是它的实现复杂且维护成本高,在大多数现代应用中收益越来越小。但在某些场景(如只有一个线程访问的 synchronized 方法)中,它的性能优势依然明显。

轻量级锁

当偏向锁被撤销后,锁升级为轻量级锁。轻量级锁假设:虽然有竞争,但线程是交替执行的,没有真正的争抢

// 轻量级锁加锁过程
// 1. 在当前线程栈帧中创建锁记录(Lock Record)空间
// 2. 用 CAS 尝试将对象的 Mark Word 替换为锁记录的指针
// 3. 如果 CAS 成功:成功获取轻量级锁
// 4. 如果 CAS 失败:先自旋重试,自旋失败后膨胀为重量级锁

// 轻量级锁的关键:不涉及操作系统的线程阻塞
// 线程通过自旋(忙等待)来等待锁释放
// 如果等待时间短,性能远高于重量级锁
// 如果等待时间长,自旋浪费 CPU

// JVM 还引入了自适应自旋(Adaptive Spinning)
// 根据上次自旋成功与否自动调整自旋次数
// - 上次自旋成功:这次多自旋几次
// - 上次自旋失败:少自旋或不自旋

重量级锁

当锁竞争真正激烈时,轻量级锁会膨胀为重量级锁:

// 重量级锁的核心:依赖操作系统 mutex
// 1. 申请 os::PlatformEvent::park() 阻塞线程
// 2. 锁释放时调用 os::PlatformEvent::unpark() 唤醒线程
// 3. 每次 park/unpark 都需要内核态切换(约 1~10μs)
// 4. 被阻塞的线程进入 Entry List 等待

// 重量级锁中还包含 cxq(竞争队列)和 EntryList(等待队列)
// 这是 JDK 内部对操作系统的优化封装
// 1. 新竞争的线程先进入 cxq(LIFO)
// 2. 被唤醒的线程从 cxq 迁移到 EntryList
// 3. 锁释放时从 EntryList 头部取一个线程唤醒

锁升级对性能的影响:

锁状态 获取成本 释放成本 适用场景
偏向锁 首次 CAS(之后无需操作) 几乎为 0 单线程访问
轻量级锁 CAS + 可能的少量自旋 CAS 低竞争交替执行
重量级锁 线程挂起 + 内核切换 线程唤醒 + 内核切换 高竞争长时间等待

2.3 synchronized 的优化技巧

// ❌ 反例:锁粒度太大
public class CoarseLock {
    public synchronized void process() {
        step1();  // 不需要同步
        step2();  // 需要同步
        step3();  // 不需要同步
        step4();  // 需要同步
    }
}

// ✅ 正例:减小锁粒度
public class FineGrainedLock {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    private final Object lock3 = new Object();

    public void process() {
        step1();
        synchronized (lock1) { step2(); }
        step3();
        synchronized (lock2) { step4(); }
    }
}

// ✅ 正例:使用细粒度锁 + 分段锁思想
public class StripedLock {
    private static final int SHARD_COUNT = 16;
    private final Object[] locks = new Object[SHARD_COUNT];
    private final Map<String, Object> cache = new HashMap<>();

    public StripedLock() {
        for (int i = 0; i < SHARD_COUNT; i++) {
            locks[i] = new Object();
        }
    }

    public Object getFromCache(String key) {
        int shard = Math.abs(key.hashCode() % SHARD_COUNT);
        synchronized (locks[shard]) {
            return cache.get(key);
        }
    }
}

三、AQS——Java 并发锁的骨架

3.1 AQS 是什么?

AbstractQueuedSynchronizer 是 Java 并发包(JUC)的基石。它是一个抽象的同步器框架,定义了多线程访问共享资源的底层模型。JUC 中大部分同步器都是基于 AQS 实现的:

同步器 基于 AQS 的模式 说明
ReentrantLock 独占模式(排他锁) 支持公平/非公平、可中断、超时
ReentrantReadWriteLock 独占 + 共享(读写锁) 读读共享、读写互斥
Semaphore 共享模式(信号量) 资源计数
CountDownLatch 共享模式(门闩) 等待线程完成
ThreadPoolExecutor.Worker 独占模式 线程包装
// AQS 的核心数据结构
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    // 核心状态:同步状态
    // 0 表示未锁定,≥1 表示已锁定
    // ReentrantLock 用它记录锁重入次数
    // Semaphore 用它记录剩余许可数
    private volatile int state;

    // 双向链表:等待队列(CLH 锁变体)
    // 头节点:当前持有锁的线程
    // 尾节点:最新入队的等待线程
    private transient volatile Node head;
    private transient volatile Node tail;
}

3.2 CLH 队列——AQS 的等待队列

AQS 内部维护了一个变体 CLH 队列(一种自旋锁的 FIFO 队列变体):

flowchart LR
    subgraph CLH 等待队列
        HEAD["head\n(当前持有者)"]
        N1["Node\nprev: head\nstatus: SIGNAL"]
        N2["Node\nprev: N1\nstatus: SIGNAL"]
        N3["Node\nprev: N2\nstatus: SIGNAL"]
        TAIL["tail\n(最新入队)"]
    end

    HEAD --> N1 --> N2 --> N3 --> TAIL

    style HEAD fill:#27ae60,color:#fff
    style TAIL fill:#e74c3c,color:#fff
// Node 节点的核心字段
static final class Node {
    // 等待状态
    static final int CANCELLED =  1;  // 取消
    static final int SIGNAL    = -1;  // 后继节点需要被唤醒
    static final int CONDITION = -2;  // 在条件队列中等待
    static final int PROPAGATE = -3; // 共享模式下传播唤醒

    volatile int waitStatus;     // 节点状态
    volatile Node prev;          // 前驱节点
    volatile Node next;          // 后继节点
    volatile Thread thread;      // 等待线程

    Node nextWaiter;  // 条件队列的后继或 Shared 标记
}

关键设计思路: CLH 队列使用的是隐式的前驱通知机制——每个节点在阻塞前会将其前驱节点的 waitStatus 设置为 SIGNAL,这样当前驱节点释放锁时,会检查其后继节点是否需要唤醒。这种方式避免了全局广播,只通知”下一个应该唤醒的线程”。

3.3 AQS 的两种模式

独占模式——以 ReentrantLock 为例

// ReentrantLock.lock() → sync.lock() → AQS.acquire(1)
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&                    // 尝试获取锁
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  // 入队等待
        selfInterrupt();                       // 被中断过则恢复中断标志
}

// tryAcquire 由子类实现——这里展示非公平锁的版本
// java.util.concurrent.locks.ReentrantLock.Sync.nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 锁空闲 → 直接尝试获取(非公平的体现)
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        // 重入:当前线程已持有锁
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;  // 获取失败,进入等待队列
}

// acquireQueued:在队列中等待
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 检查是否需要 park(阻塞)
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

释放锁的过程:

public final boolean release(int arg) {
    if (tryRelease(arg)) {  // 子类实现:减少 state
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);  // 唤醒后继节点
        return true;
    }
    return false;
}

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);  // 清除 SIGNAL

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {  // 后继被取消 → 从尾部向前找有效节点
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);  // 唤醒
}

共享模式——以 Semaphore / CountDownLatch 为例

// 共享模式的 acquire 流程
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)  // 子类实现:返回剩余资源数
        doAcquireShared(arg);
}

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);  // 关键区别:传播!
                    p.next = null;
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

// 共享模式释放时的传播
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head;
    setHead(node);

    // propagate > 0 表示还有剩余资源 → 继续唤醒后继
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();  // 传播唤醒后面的共享节点
    }
}

独占 vs 共享的核心差异:

特性 独占模式 共享模式
state 语义 0=空闲, ≥1=重入次数 剩余资源量
唤醒行为 只唤醒下一个节点 传播唤醒所有共享节点
典型实现 ReentrantLock Semaphore, CountDownLatch
条件变量 支持(Condition) 不支持

3.4 公平锁 vs 非公平锁

// 非公平锁的 tryAcquire(前面已展示)
// 非公平:新来的线程直接 CAS 抢锁,不管队列中有没有等待者
// 优点:吞吐量高(减少线程 park/unpark 开销)
// 缺点:可能导致等待线程"饿死"

// 公平锁的 tryAcquire
static final class FairSync extends Sync {
    @ReservedStackAccess
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 关键区别:先检查队列中是否有等待者
            if (!hasQueuedPredecessors() &&  // 没有等待者才尝试
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

// hasQueuedPredecessors:检查队列中是否有线程等待时间更长
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

性能对比(JMH 基准测试):

竞争程度 公平锁 非公平锁 差异
低竞争(10线程) 85ms 82ms ~3%
中竞争(100线程) 310ms 215ms ~30%
高竞争(1000线程) 2800ms 980ms ~65%

非公平锁在高竞争场景下的优势更明显,因为避免了为”排队”付出线程切换代价。官方推荐默认使用非公平锁。

3.5 Condition——条件变量的实现

Condition 是 AQS 中的另一个重要组件,提供了类似 Object.wait/notify 的线程协作机制:

public class ConditionObject implements Condition, java.io.Serializable {
    // 条件队列(单向链表)
    private transient Node firstWaiter;
    private transient Node lastWaiter;

    // await() → 释放锁 + 进入条件队列等待
    public final void await() throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();

        Node node = addConditionWaiter();     // 加入条件队列
        int savedState = fullyRelease(node);  // 释放当前线程持有的锁
        int interruptMode = 0;

        // 循环等待:直到被转移到同步队列
        while (!isOnSyncQueue(node)) {
            LockSupport.park(this);           // 阻塞
            if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                break;
        }

        // 重新获取锁
        if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
            interruptMode = REINTERRUPT;
        if (node.nextWaiter != null)
            unlinkCancelledWaiters();
        if (interruptMode != 0)
            reportInterruptAfterWait(interruptMode);
    }

    // signal() → 将条件队列头节点转移到同步队列
    public final void signal() {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        Node first = firstWaiter;
        if (first != null)
            doSignal(first);
    }

    // 转移节点到同步队列
    private void doSignal(Node first) {
        do {
            if ((firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            first.nextWaiter = null;
        } while (!transferForSignal(first) &&   // 转移到同步队列
                 (first = firstWaiter) != null);
    }
}
flowchart LR
    subgraph 条件队列
        CW1[Condition Waiter-1]
        CW2[Condition Waiter-2]
        CW3[Condition Waiter-3]
        CW1 --> CW2 --> CW3
    end

    subgraph 同步队列(CLH
        SW1[Node head\n持有锁]
        SW2[Node-2]
        SW3[Node-3]
        SW1 --> SW2 --> SW3
    end

    signal -->|转移第一个条件等待节点| CW1
    CW1 -->|transferForSignal| SW2

    style CW1 fill:#25a25a,color:#fff
    style CW2 fill:#f39c12
    style CW3 fill:#f39c12
    style SW1 fill:#27ae60,color:#fff

四、synchronized vs AQS——如何选择?

// synchronized 适用场景:
// 1. 简单的方法级同步
// 2. 无需超时、可中断等高级特性
// 3. 代码自动管理锁获取和释放(不易出错)
public synchronized void simpleOp() {
    // 一行代码能解决的问题
    this.count++;
}

// AQS 适用场景:
// 1. 需要超时获取锁
// 2. 需要可中断的锁
// 3. 需要公平性
// 4. 需要多个条件变量(Condition)
// 5. 需要读写锁分离
// 6. 需要实现自定义同步器
public void advancedOp() {
    ReentrantLock lock = new ReentrantLock();
    Condition notFull = lock.newCondition();
    Condition notEmpty = lock.newCondition();

    try {
        if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
            try {
                while (queue.isFull()) {
                    notFull.await();  // 等待条件
                }
                queue.put(item);
                notEmpty.signal();
            } finally {
                lock.unlock();
            }
        } else {
            // 超时处理
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

关键对比表:

特性 synchronized ReentrantLock (AQS)
语法简洁性 ✅ 自动管理 ❌ 需要手动加解锁
锁升级 ✅ 偏向→轻量→重量 N/A
可中断
超时获取
公平性 ❌ 非公平 ✅ 可配置
多条件变量 ❌ 单条件(notify) ✅ 多 Condition
读写分离 ✅ ReadWriteLock
是否支持虚拟线程 ✅ 推荐 ⚠️ 尽量使用 synchronized
异常安全 ✅ 自动释放 ⚠️ 必须 finally 释放

五、LockSupport——底层线程阻塞原语

无论是 AQS 还是 synchronized,底层都依赖于 LockSupport 来进行线程的阻塞和唤醒:

public class LockSupport {
    // 本质是调用 Unsafe.park() / unpark()
    // park 会检查当前线程的"许可"(permit)
    // - 如果有许可:立即返回并消费许可
    // - 如果没有许可:阻塞等待

    // 线程阻塞
    public static void park() {
        UNSAFE.park(false, 0L);
    }

    public static void parkNanos(long nanos) {
        if (nanos > 0)
            UNSAFE.park(false, nanos);
    }

    // 线程唤醒(提前给许可)
    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }
}

与 Object.wait/notify 不同,LockSupport 的 park/unpark 是”许可证机制”,不需要在同步块中调用。这意味着 unpark 可以在 park 之前被调用——如果提前调用了 unpark,后续的 park 会直接返回(因为许可已被消费)。

六、实战最佳实践

// 1. 优先使用 synchronized(除非需要高级功能)
// 理由:synchronized 在 JDK 中持续优化,且与虚拟线程兼容性更好

// 2. 使用读写锁分离读多写少的场景
public class ReadWriteCache {
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock r = rwl.readLock();
    private final Lock w = rwl.writeLock();
    private Map<String, Object> cache = new HashMap<>();

    public Object get(String key) {
        r.lock();
        try {
            return cache.get(key);
        } finally {
            r.unlock();
        }
    }

    public void put(String key, Object value) {
        w.lock();
        try {
            cache.put(key, value);
        } finally {
            w.unlock();
        }
    }
}

// 3. 锁的粒度尽量小——能锁代码块绝不锁方法
// 4. 尽量避免锁嵌套(死锁风险)
// 5. 高竞争场景使用 LongAdder 替代 AtomicLong 再替代 synchronized

七、总结

  1. synchronized 经历了从重量级到轻量级的蜕变。 锁升级机制让 synchronized 在低竞争场景下的性能几乎与无锁代码相当。理解偏向锁、轻量级锁、重量级锁的演进路径,是深入 JVM 性能调优的必备知识。

  2. AQS 是 JUC 的基石。 它通过 state + CLH 队列实现了可扩展的同步框架。独占模式、共享模式、条件变量的设计为上层同步器提供了统一的基础设施。

  3. 锁的选择需要权衡。 synchronized 足够简单且性能优异,适合 90% 的场景。但在需要超时、可中断、多条件变量、读写锁分离等高级特性时,AQS 的实现(ReentrantLock、ReadWriteLock 等)不可替代。

  4. 底层都依赖操作系统。 无论是 synchronized 最终膨胀为 mutex,还是 AQS 使用 LockSupport.park(),最终都会触发操作系统的线程调度。理解这一点是理解并发性能瓶颈的关键。

  5. 面向虚拟线程的新时代。 JDK 21 引入虚拟线程后,传统的”用线程池处理高并发”模式正在被”创建大量轻量级线程”所取代。在虚拟线程场景下,synchronized 是首选锁机制,因为它在虚拟线程的调度下表现更好。


Java 并发核心机制深入拆解:从 synchronized 到 AQS 的底层真相


title: “Java 并发核心机制深入拆解:从 synchronized 到 AQS 的底层真相”

一、引言

并发编程是 Java 开发者绕不开的核心能力。从最简单的 synchronized 关键字到复杂的 ReentrantLockThreadLocal,每一层抽象背后都隐藏着操作系统、内存模型和 CPU 架构的深刻考量。本文将从字节码与源码两个维度,逐层拆解 Java 并发体系的五大核心机制。

二、synchronized 的原理与优化演进

2.1 字节码层面的真相

synchronized 在 Java 代码中有三种使用形态:

// 1. 修饰实例方法
public synchronized void instanceMethod() { }
// 2. 修饰静态方法
public static synchronized void staticMethod() { }
// 3. 修饰代码块
public void blockMethod() {
    synchronized (this) { }
}

通过 javap -v 反编译,三种形态的字节码表现完全不同:

实例方法(ACC_SYNCHRONIZED 标记):

public synchronized void instanceMethod();
  flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
    stack=0, locals=1, args_size=1
    0: return

代码块(monitorenter/monitorexit):

 3: monitorenter
 4: aload_1
 5: monitorexit
 6: goto 14
 9: astore_2
10: aload_1
11: monitorexit
12: athrow

关键区别在于:方法级的 synchronized 通过方法标志位隐式实现,而代码块级则依赖显式的 monitorentermonitorexit 指令。无论哪种形式,最终都依赖于底层 ObjectMonitor 机制。

2.2 锁升级:从无锁到重量级

JDK 1.6 之后,synchronized 经历了从”重量级”到”自适应”的重大优化,形成了无锁 → 偏向锁 → 轻量级锁 → 重量级锁的升级路径:

flowchart LR
    A[无锁] -->|"线程获取锁"| B[偏向锁]
    B -->|"竞争出现"| C[轻量级锁]
    C -->|"自旋失败/竞争加剧"| D[重量级锁]
    D -->|"锁释放"| A
    style A fill:#4CAF50,color:white
    style B fill:#2196F3,color:white
    style C fill:#FF9800,color:white
    style D fill:#f44336,color:white

偏向锁(Biased Locking): 锁对象头中的 Mark Word 记录持有锁的线程 ID。单线程重复获取时无需 CAS 操作,延迟极低。JDK 15 开始默认禁用,JDK 21 已移除。

轻量级锁(Lightweight Lock): 通过 CAS 在对象头 Mark Word 中记录指向当前线程栈帧中 Lock Record 的指针。不涉及操作系统内核切换,通过自旋等待。

重量级锁(Heavyweight Lock): 当自旋超过阈值(默认 10 次,可自适应调整),锁膨胀为重量级锁,Mark Word 指向 ObjectMonitor 对象,线程被阻塞在 _WaitSet_EntryList 上,需要操作系统内核态切换(pthread_mutex_lock)。

flowchart TD
    subgraph Mark Word 结构
        M[32/64  Mark Word]
        M --> M1[偏向锁: 线程ID + Epoch + 分代年龄 + 偏向位]
        M --> M2[轻量锁: 指向 Lock Record 的指针]
        M --> M3[重量锁: 指向 ObjectMonitor 的指针]
        M --> M4[GC 标记: 标记阶段专用]
    end

2.3 锁升级的性能对比

锁状态 CAS 开销 内核切换 适用场景 延迟
偏向锁 0 0 单线程重复获取 <10ns
轻量级锁 1次CAS 0 低竞争、短持有 ~50ns
重量级锁 多次CAS 1~2次 高竞争、长持有 ~10μs

反直觉的结论: 重量级锁在极度高竞争下反而比 CAS-heavy 的轻量级锁更高效,因为线程被挂起后不会浪费 CPU 自旋。

三、AQS 框架与 ReentrantLock 实现

3.1 AQS 核心架构

AbstractQueuedSynchronizer(AQS)是 JUC 锁与同步器的基石。ReentrantLockCountDownLatchSemaphoreReentrantReadWriteLock 均基于它实现。

// AQS 核心字段
public abstract class AbstractQueuedSynchronizer {
    // 同步状态,volatile 保证可见性
    private volatile int state;

    // CLH 等待队列的头尾节点
    private transient volatile Node head;
    private transient volatile Node tail;
}

AQS 的设计精髓在于 模板方法模式。子类只需实现 tryAcquire/tryRelease(独占模式)或 tryAcquireShared/tryReleaseShared(共享模式),而队列管理、阻塞唤醒、中断处理等复杂逻辑由 AQS 统一完成。

3.2 CLH 队列与 Node 节点

AQS 内部维护了一个 CLH(Craig, Landin, Hagersten)锁的变体——显式队列 + 自旋 + 阻塞:

static final class Node {
    // 节点模式:共享/独占
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;

    // 等待状态:CANCELLED=1, SIGNAL=-1, CONDITION=-2, PROPAGATE=-3
    volatile int waitStatus;

    volatile Node prev;    // 前驱
    volatile Node next;    // 后继
    volatile Thread thread; // 关联线程
    Node nextWaiter;        // 条件队列的下一节点
}

关键机制流程:

flowchart TD
    A[尝试获取锁 tryAcquire] -->|成功| B[执行业务逻辑]
    A -->|失败| C[创建 Node 加入 CLH 队列]
    C --> D[自旋检查前驱是否为 head]
    D -->|是且 tryAcquire 成功| E[设为新 head 执行业务]
    D -->|| F[parkAndCheckInterrupt 阻塞]
    F --> G[被前驱线程 unpark 唤醒]
    G --> D
    E --> H[释放锁 setState+unpark 后继]

3.3 ReentrantLock 实现细节

public class ReentrantLock implements Lock {
    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {
        // 非公平锁:直接 CAS 抢一次
        // 公平锁:hasQueuedPredecessors() 才 CAS
    }

    static final class NonfairSync extends Sync {
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1); // 进入 AQS
        }
    }

    static final class FairSync extends Sync {
        final void lock() {
            acquire(1); // 直接进 AQS,无插队
        }
    }
}

公平锁 vs 非公平锁的核心差异:

特性 非公平锁 公平锁
首次获取 直接 CAS 抢锁 检查队列是否有等待者
吞吐量 更高(减少线程切换) 较低
公平性 可能”插队”导致饥饿 严格 FIFO
适用场景 大部分业务场景 对公平性敏感的金融交易

反直觉的点: 非公平锁虽然不公平,但整体吞吐量更高。因为唤醒一个线程需要上下文切换,而正在运行的线程直接获取锁可以省去这次切换,让刚释放锁的线程继续运行。

3.4 Condition 的实现

Conditionsynchronizedwait/notify 对应,但更灵活:

class BoundedBuffer {
    final Lock lock = new ReentrantLock();
    final Condition notFull  = lock.newCondition();
    final Condition notEmpty = lock.newCondition();

    void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length) notFull.await();
            // ... 插入数据
            notEmpty.signal();
        } finally { lock.unlock(); }
    }
}

Condition.await() 将当前线程从 AQS 同步队列移到 条件队列firstWaiter/lastWaiter 单向链表),释放锁并挂起。被 signal() 后,线程从条件队列取出,重新加入到同步队列竞争锁。

四、volatile 与 happens-before 规则

4.1 volatile 的语义

volatile 是 Java 提供的最轻量级的同步机制,具备两个核心语义:

  1. 可见性: 对一个 volatile 变量的写操作,后续对其他线程的读操作立即可见
  2. 禁止重排序: volatile 读写操作前后不能重排序
// volatile 的典型使用场景:状态标志
volatile boolean running = true;

// 线程1
public void stop() {
    running = false;  // volatile 写 → StoreLoad 屏障
}

// 线程2
public void run() {
    while (running) { // volatile 读
        // 业务逻辑
    }
}

4.2 内存屏障的实现

在 x86 架构下,volatile 写会在指令前插入 StoreStore 屏障(防止前面的普通写被重排序到 volatile 写之后),在指令后插入 StoreLoad 屏障(防止后面的 volatile 读/写被重排序到 volatile 写之前)。

flowchart TD
    subgraph "正常代码顺序"
        A[普通写操作]
        B[volatile 写]
        C[普通读操作]
    end
    subgraph "内存屏障效果"
        D[普通写操作]
        E[StoreStore 屏障]
        F[volatile 写]
        G[StoreLoad 屏障]
        H[普通读操作]
    end

4.3 happens-before 规则

JMM(Java Memory Model)通过 happens-before 规则定义多线程操作的 偏序关系。如果 A happens-before B,则 A 的操作结果对 B 可见:

  1. 程序顺序规则: 单线程内,前面的操作 happens-before 后续操作
  2. 监视器锁规则: 解锁操作 happens-before 后续加锁
  3. volatile 规则: volatile 写 happens-before 后续读
  4. 传递性: A happens-before B,B happens-before C → A happens-before C
  5. 线程启动规则: Thread.start() happens-before 该线程的任何操作
  6. 线程中断规则: interrupt() happens-before 检测到中断
  7. 线程终止规则: join() 返回前的所有操作 happens-before join 之后
// 经典案例:没有 happens-before 的错误代码
int a = 0, b = 0;
boolean flag = false;

// 线程1
a = 1;
b = 2;
flag = true;   // volatile 写

// 线程2
if (flag) {     // volatile 读
    // a 和 b 一定可见,因为 volatile 写 happens-before volatile 读
    assert a == 1 && b == 2; // 永远为 true
}

五、ThreadLocal 源码深度分析

5.1 数据结构

ThreadLocal 并不直接存储值,而是以 ThreadLocalMap 的形式存储在 Thread 对象上:

// Thread 类中的字段
public class Thread {
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
flowchart LR
    T1[Thread-1] --> M1[ThreadLocalMap]
    M1 --> E1[Entry: key=TL1, value=v1]
    M1 --> E2[Entry: key=TL2, value=v2]
    T2[Thread-2] --> M2[ThreadLocalMap]
    M2 --> E3[Entry: key=TL1, value=v3]
    M2 --> E4[Entry: key=TL2, value=v4]

不用 Map 而是用 Entry[] 的原因: ThreadLocalMap 使用开放地址法(线性探测)而非拉链法解决哈希冲突,因为 Entry 数量通常很小(大多数线程只有少数几个 ThreadLocal 变量),开放地址法缓存更友好。

5.2 弱引用与内存泄漏

每个 Entry 的 key 是 弱引用(WeakReference)

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal> {
        Object value;
        Entry(ThreadLocal k, Object v) {
            super(k); // 弱引用
            value = v;
        }
    }
}

为什么要用弱引用? 如果 key 是强引用,ThreadLocal 对象在线程存活期间永远不会被 GC,造成内存泄漏。弱引用允许 ThreadLocal 被回收,此时 Entry 的 key 变为 null。

状态演进:
① ThreadLocal 强引用 → Entry key 弱引用 → value 强引用
② ThreadLocal 不再使用(=null)后
③ GC 回收 ThreadLocal → Entry.key = null
④ value 仍然存在 → **内存泄漏!**

正确的使用方式: 每次使用完毕调用 remove() 方法,或者在 finally 块中清理。

// 正确用法
ThreadLocal<Connection> conn = new ThreadLocal<>();
try {
    conn.set(dataSource.getConnection());
    // ... 操作
} finally {
    conn.remove(); // 务必清理
}

5.3 哈希算法

ThreadLocalMap 使用 斐波那契哈希(黄金分割数)来生成散列值:

public class ThreadLocal<T> {
    private final int threadLocalHashCode = nextHashCode();

    private static AtomicInteger nextHashCode =
        new AtomicInteger();
    private static final int HASH_INCREMENT = 0x61c88647; 
    // 黄金分割数

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
}

0x61c88647 是 2^32 的黄金比例对应的整数。利用这个魔数生成的哈希值在 2 的幂次方的数组中几乎均匀分布,且相邻 ThreadLocal 的哈希值间隔固定,减少了线性探测的冲突。

5.4 InheritableThreadLocal

InheritableThreadLocal 允许子线程继承父线程的 ThreadLocal 值。实现原理是在 Thread.init() 创建子线程时,将父线程的 inheritableThreadLocals 复制到子线程:

private void init(ThreadGroup g, Runnable target, ...) {
    // ...
    if (parent.inheritableThreadLocals != null) {
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(
                parent.inheritableThreadLocals);
    }
}

注意: 对于线程池场景,InheritableThreadLocal 会失效(线程复用不会重新继承),需要使用 TransmittableThreadLocal(阿里开源的 TTL)。

六、总结

机制 核心特点 适用场景 注意事项
synchronized 自动管理、锁升级、简洁 短操作、低竞争 不能超时,阻塞不可中断
ReentrantLock 可中断、可超时、公平可选 长操作、高竞争 手动 unlock,需 finally
volatile 轻量级、无阻塞 状态标志、double-check 不保证原子性
ThreadLocal 线程隔离、无竞争 上下文传递、连接池 必须 remove,防泄漏

Java 并发体系的精妙之处在于:上层语法简单(synchronized),下层实现复杂(锁升级);上层语义明确(volatile 可见性),下层硬件屏障支撑;上层抽象易用(AQS),下层实现灵活(模板方法)。 理解这些底层机制是成为 Java 高级开发者的必经之路。

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容