📌 本文由 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 上能跑 |
九、总结
-
JMM 定义了 Java 并发编程的”宪法”。 它规定了线程之间如何通过共享内存通信,以及什么情况下的重排是可见的。”happens-before”规则是理解 JMM 的钥匙。
-
volatile 是最轻量的线程同步机制。 它提供了可见性和有序性,但不提供原子性。合理使用 volatile(状态标志、一次性安全发布等场景)可以减少对锁的依赖,提升性能。
-
内存屏障是 JMM 的硬件映射。 不同 CPU 架构对 JMM 的支持程度不同。x86 的能力最强(只在 StoreLoad 需要硬件屏障),ARM/PowerPC 需要更多的硬件屏障指令。这也是为什么在 x86 上测试通过的并发代码,迁移到 ARM 服务器上可能出现问题。
-
final 在 JSR 133 后获得了增强的内存语义。 正确构造的 final 字段可以安全地在多线程间共享,这是不可变对象并发安全的基础保证。
-
Happens-Before 规则链是分析并发问题的第一步。 在讨论任何并发问题之前,先画出 happens-before 关系链,这是比阅读代码本身更重要的步骤。
-
工具辅助分析。 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
七、总结
-
synchronized 经历了从重量级到轻量级的蜕变。 锁升级机制让 synchronized 在低竞争场景下的性能几乎与无锁代码相当。理解偏向锁、轻量级锁、重量级锁的演进路径,是深入 JVM 性能调优的必备知识。
-
AQS 是 JUC 的基石。 它通过 state + CLH 队列实现了可扩展的同步框架。独占模式、共享模式、条件变量的设计为上层同步器提供了统一的基础设施。
-
锁的选择需要权衡。 synchronized 足够简单且性能优异,适合 90% 的场景。但在需要超时、可中断、多条件变量、读写锁分离等高级特性时,AQS 的实现(ReentrantLock、ReadWriteLock 等)不可替代。
-
底层都依赖操作系统。 无论是 synchronized 最终膨胀为 mutex,还是 AQS 使用 LockSupport.park(),最终都会触发操作系统的线程调度。理解这一点是理解并发性能瓶颈的关键。
-
面向虚拟线程的新时代。 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 用一个 AtomicInteger(ctl)同时管理两个状态:线程池运行状态(高 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(); // 必须清理!
}
});
七、总结
-
ThreadPoolExecutor 的 7 大参数是一个有机整体。 corePoolSize 决定基础并发量,workQueue 承载缓冲,maximumPoolSize 控制峰值弹性,keepAliveTime 管理资源释放,ThreadFactory 提供线程信息,RejectedExecutionHandler 处理过载。任何参数的错误配置都可能引发严重问题。
-
活用有界队列 + CallerRunsPolicy。 这组组合是生产环境最推荐的配置。有界队列防止 OOM,CallerRunsPolicy 提供隐式背压机制。
-
理解 ctl 的设计哲学。 一个 AtomicInteger 同时管理状态和线程数,用位运算实现无锁的状态判断,是 JDK 源码中”用复杂技巧解决简单问题”的精妙案例。
-
善用监控。 线程池的黑盒特性决定了必须持续监控活跃线程数、队列深度、拒绝次数等指标。这些指标是定位性能问题的最直接线索。
-
警惕线程池陷阱。 异常吞没、任务依赖死锁、ThreadLocal 污染、关闭不当——这些是生产环境最常遇到的问题,需要在代码中预设保护机制。
-
动态调整让你的线程池”活起来”。 ThreadPoolExecutor 提供了运行时调整参数的接口,结合监控数据可以实现自动伸缩。
Java 并发核心机制深入拆解:从 synchronized 到 AQS 的底层真相
title: “Java 并发核心机制深入拆解:从 synchronized 到 AQS 的底层真相”
一、引言
并发编程是 Java 开发者绕不开的核心能力。从最简单的 synchronized 关键字到复杂的 ReentrantLock、ThreadLocal,每一层抽象背后都隐藏着操作系统、内存模型和 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 通过方法标志位隐式实现,而代码块级则依赖显式的 monitorenter 和 monitorexit 指令。无论哪种形式,最终都依赖于底层 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 锁与同步器的基石。ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock 均基于它实现。
// 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 的实现
Condition 与 synchronized 的 wait/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 提供的最轻量级的同步机制,具备两个核心语义:
- 可见性: 对一个 volatile 变量的写操作,后续对其他线程的读操作立即可见
- 禁止重排序: 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 可见:
- 程序顺序规则: 单线程内,前面的操作 happens-before 后续操作
- 监视器锁规则: 解锁操作 happens-before 后续加锁
- volatile 规则: volatile 写 happens-before 后续读
- 传递性: A happens-before B,B happens-before C → A happens-before C
- 线程启动规则:
Thread.start()happens-before 该线程的任何操作 - 线程中断规则:
interrupt()happens-before 检测到中断 - 线程终止规则:
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(降级)
面试要点
- InnoDB 无真正的锁升级:不像 SQL Server 有行→页→表的自动升级机制
- 唯一索引降级:唯一索引等值查询时,Next-Key Lock 降级为 Record Lock
- 隔离级别影响:RC 下所有当前读都使用 Record Lock(相对 RR 的降级)
- 无索引的后果:看起来像”升级”为全表加锁
- 面试考点:”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_timeout 和 innodb_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 和索引,减少锁持有时间
面试要点
- 默认值:50 秒,可以动态调整
- 与死锁检测的区别:锁等待超时是时间阈值触发,死锁检测是图算法触发
- 回滚范围:只回滚超时语句,不是整个事务 —— 需要应用程序额外处理
- 配置建议:OLTP 系统设置 5-10 秒,批量处理系统设置 30-60 秒
- 监控方法:
sys.innodb_lock_waits或information_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,+∞)
间隙锁可以锁定:
– 两条索引记录之间的间隙
– 第一条索引记录之前的间隙
– 最后一条索引记录之后的间隙
间隙锁的特点
- 不锁定记录本身:间隙锁只阻止插入,不阻止对已有记录的修改
- 锁的是间隙范围:阻止其他事务在该间隙中插入任何行
- 间隙锁可以共存:多个事务可以对同一个间隙加 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. 使用索引设计规避
- 唯一索引的等值查询不会产生间隙锁
- 如果非唯一列也只需要等值查询,考虑加唯一索引
面试要点
- 间隙锁 ≠ 记录锁:记录锁锁行,间隙锁锁间隙
- 核心作用:阻止幻读(防止其他事务插入新行)
- 仅在 REPEATABLE READ 和 SERIALIZABLE 下生效
- 间隙锁之间不冲突,只和 INSERT 冲突
- 间隙锁是一把双刃剑——解决幻读的同时增加死锁概率、降低并发
- 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. 因此下次同样的查询不会多出"幻影"记录
临键锁的缺点
- 范围过大:范围查询可能导致大量的间隙被锁定
- 死锁风险:多个事务的间隙锁交叉可能引发死锁
- 并发下降:锁的范围越大,阻塞的事务越多
- 难以预测:锁的范围受实际数据分布影响
如何查看临键锁
-- 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 表示纯间隙锁
面试要点
- Next-Key Lock = Record Lock + Gap Lock,范围是
(前值, 当前值] - 核心作用:在 REPEATABLE READ 下防止幻读
- 唯一索引+已存在行:降级为记录锁
- 非唯一索引:临键锁 + 额外间隙锁
- 到达索引边界:退化为间隙锁
- 如何查看:
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 Lock(RR 级别下)
│
▼
事务 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 升序)。
面试要点
- 本质:Record Lock 是 InnoDB 行锁的基础,锁定的是索引记录
- 必须通过索引:没有索引时退化为全表扫描加锁
- 两种类型:S 锁(共享)和 X 锁(排他),S 锁可共存,X 锁互斥
- 隔离级别影响:RC 下只有 Record Lock,RR 下升级为 Next-Key Lock
- 与间隙锁的关系: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,+∞)
间隙锁可以锁定:
– 两条索引记录之间的间隙
– 第一条索引记录之前的间隙
– 最后一条索引记录之后的间隙
间隙锁的特点
- 不锁定记录本身:间隙锁只阻止插入,不阻止对已有记录的修改
- 锁的是间隙范围:阻止其他事务在该间隙中插入任何行
- 间隙锁可以共存:多个事务可以对同一个间隙加 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. 使用索引设计规避
- 唯一索引的等值查询不会产生间隙锁
- 如果非唯一列也只需要等值查询,考虑加唯一索引
面试要点
- 间隙锁 ≠ 记录锁:记录锁锁行,间隙锁锁间隙
- 核心作用:阻止幻读(防止其他事务插入新行)
- 仅在 REPEATABLE READ 和 SERIALIZABLE 下生效
- 间隙锁之间不冲突,只和 INSERT 冲突
- 间隙锁是一把双刃剑——解决幻读的同时增加死锁概率、降低并发
- 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. 因此下次同样的查询不会多出"幻影"记录
临键锁的缺点
- 范围过大:范围查询可能导致大量的间隙被锁定
- 死锁风险:多个事务的间隙锁交叉可能引发死锁
- 并发下降:锁的范围越大,阻塞的事务越多
- 难以预测:锁的范围受实际数据分布影响
如何查看临键锁
-- 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 表示纯间隙锁
面试要点
- Next-Key Lock = Record Lock + Gap Lock,范围是
(前值, 当前值] - 核心作用:在 REPEATABLE READ 下防止幻读
- 唯一索引+已存在行:降级为记录锁
- 非唯一索引:临键锁 + 额外间隙锁
- 到达索引边界:退化为间隙锁
- 如何查看:
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 下运行良好,但在主从架构中存在一个致命缺陷:
- 客户端 A 向 Master 写入锁
- Master 在将锁同步到 Slave 之前宕机
- Slave 提升为新的 Master
- 客户端 B 向新的 Master 获取同一把锁——成功了!
- 这时客户端 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
步骤四:判断是否加锁成功
如果同时满足以下两个条件,则加锁成功:
- 超过半数节点(N/2 + 1)加锁成功
- 加锁总耗时小于锁的有效期(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 没有解决的问题
- 性能下降:需要和 5 个节点交互,延迟增加
- 时钟依赖:假设各节点时钟一致,如果节点间时钟偏差大,算法可能失效
- 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: Lock 和 RLock 在性能上有区别吗?
A: RLock 比 Lock 稍慢(需要维护持有线程和重入计数)。如果你不需要重入特性,用 Lock 性能略优。
Q: threading.Lock 和 threading.Semaphore 有什么区别?
A: Lock 是互斥的(一次只有一个线程),Semaphore 允许多个线程同时访问(计数信号量)。Semaphore(1) 等价于 Lock。
Q: 锁竞争太激烈怎么办?
A: 策略包括:减小临界区、用读写锁替代互斥锁、用原子操作替代锁(queue.Queue、multiprocessing.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 方式检测
最佳实践
- 只在必要时使用:默认情况下不需要 LockOSThread。
- 始终配对:LockOSThread 和 UnlockOSThread 必须成对出现,推荐用
defer。 - 尽早调用:在 goroutine 函数的最开始就调用 LockOSThread。
- 避免长时间绑定:完成必要操作后尽快解锁。
- 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() 会阻塞当前线程直到任务完成;② 需处理 InterruptedException 和 ExecutionException;③ 建议使用带超时的 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 Word 和 Monitor(管程/监视器锁)。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 代码块在字节码层面通过 monitorenter 和 monitorexit 指令实现:
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++ 的方式:synchronized、AtomicInteger、ReentrantLock。
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区别详解:同步机制的对比与选择
一、定义
volatile 和 synchronized 是 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++) | synchronized 或 AtomicInteger |
需要原子性 |
| 复合操作(check-then-act) | synchronized 或 Lock |
需要原子性+可见性 |
| 发布不可变对象 | volatile |
不可变对象引用本身是安全的 |
| 缓存池/累加器 | synchronized 或 LongAdder |
高并发下的性能优化 |
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:两者在性能上差多少?
volatile 比 synchronized 轻量 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区别详解:显式锁与隐式锁的全面对比
一、定义
Lock 是 java.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 用一个 AtomicInteger(ctl)同时管理两个状态:线程池运行状态(高 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(); // 必须清理!
}
});
七、总结
-
ThreadPoolExecutor 的 7 大参数是一个有机整体。 corePoolSize 决定基础并发量,workQueue 承载缓冲,maximumPoolSize 控制峰值弹性,keepAliveTime 管理资源释放,ThreadFactory 提供线程信息,RejectedExecutionHandler 处理过载。任何参数的错误配置都可能引发严重问题。
-
活用有界队列 + CallerRunsPolicy。 这组组合是生产环境最推荐的配置。有界队列防止 OOM,CallerRunsPolicy 提供隐式背压机制。
-
理解 ctl 的设计哲学。 一个 AtomicInteger 同时管理状态和线程数,用位运算实现无锁的状态判断,是 JDK 源码中”用复杂技巧解决简单问题”的精妙案例。
-
善用监控。 线程池的黑盒特性决定了必须持续监控活跃线程数、队列深度、拒绝次数等指标。这些指标是定位性能问题的最直接线索。
-
警惕线程池陷阱。 异常吞没、任务依赖死锁、ThreadLocal 污染、关闭不当——这些是生产环境最常遇到的问题,需要在代码中预设保护机制。
-
动态调整让你的线程池”活起来”。 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 上能跑 |
九、总结
-
JMM 定义了 Java 并发编程的”宪法”。 它规定了线程之间如何通过共享内存通信,以及什么情况下的重排是可见的。”happens-before”规则是理解 JMM 的钥匙。
-
volatile 是最轻量的线程同步机制。 它提供了可见性和有序性,但不提供原子性。合理使用 volatile(状态标志、一次性安全发布等场景)可以减少对锁的依赖,提升性能。
-
内存屏障是 JMM 的硬件映射。 不同 CPU 架构对 JMM 的支持程度不同。x86 的能力最强(只在 StoreLoad 需要硬件屏障),ARM/PowerPC 需要更多的硬件屏障指令。这也是为什么在 x86 上测试通过的并发代码,迁移到 ARM 服务器上可能出现问题。
-
final 在 JSR 133 后获得了增强的内存语义。 正确构造的 final 字段可以安全地在多线程间共享,这是不可变对象并发安全的基础保证。
-
Happens-Before 规则链是分析并发问题的第一步。 在讨论任何并发问题之前,先画出 happens-before 关系链,这是比阅读代码本身更重要的步骤。
-
工具辅助分析。 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
七、总结
-
synchronized 经历了从重量级到轻量级的蜕变。 锁升级机制让 synchronized 在低竞争场景下的性能几乎与无锁代码相当。理解偏向锁、轻量级锁、重量级锁的演进路径,是深入 JVM 性能调优的必备知识。
-
AQS 是 JUC 的基石。 它通过 state + CLH 队列实现了可扩展的同步框架。独占模式、共享模式、条件变量的设计为上层同步器提供了统一的基础设施。
-
锁的选择需要权衡。 synchronized 足够简单且性能优异,适合 90% 的场景。但在需要超时、可中断、多条件变量、读写锁分离等高级特性时,AQS 的实现(ReentrantLock、ReadWriteLock 等)不可替代。
-
底层都依赖操作系统。 无论是 synchronized 最终膨胀为 mutex,还是 AQS 使用 LockSupport.park(),最终都会触发操作系统的线程调度。理解这一点是理解并发性能瓶颈的关键。
-
面向虚拟线程的新时代。 JDK 21 引入虚拟线程后,传统的”用线程池处理高并发”模式正在被”创建大量轻量级线程”所取代。在虚拟线程场景下,synchronized 是首选锁机制,因为它在虚拟线程的调度下表现更好。
Java 并发核心机制深入拆解:从 synchronized 到 AQS 的底层真相
title: “Java 并发核心机制深入拆解:从 synchronized 到 AQS 的底层真相”
一、引言
并发编程是 Java 开发者绕不开的核心能力。从最简单的 synchronized 关键字到复杂的 ReentrantLock、ThreadLocal,每一层抽象背后都隐藏着操作系统、内存模型和 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 通过方法标志位隐式实现,而代码块级则依赖显式的 monitorenter 和 monitorexit 指令。无论哪种形式,最终都依赖于底层 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 锁与同步器的基石。ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock 均基于它实现。
// 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 的实现
Condition 与 synchronized 的 wait/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 提供的最轻量级的同步机制,具备两个核心语义:
- 可见性: 对一个 volatile 变量的写操作,后续对其他线程的读操作立即可见
- 禁止重排序: 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 可见:
- 程序顺序规则: 单线程内,前面的操作 happens-before 后续操作
- 监视器锁规则: 解锁操作 happens-before 后续加锁
- volatile 规则: volatile 写 happens-before 后续读
- 传递性: A happens-before B,B happens-before C → A happens-before C
- 线程启动规则:
Thread.start()happens-before 该线程的任何操作 - 线程中断规则:
interrupt()happens-before 检测到中断 - 线程终止规则:
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 高级开发者的必经之路。


暂无评论内容