Java 多线程与并发面试核心总结
一、基础概念
1.1 线程与进程
| 维度 | 进程(Process) | 线程(Thread) |
|---|---|---|
| 定义 | 操作系统资源分配的最小单位 | CPU 调度执行的最小单位 |
| 资源拥有 | 独立的内存地址空间、文件描述符、信号处理等 | 共享进程的堆和方法区,独有栈、程序计数器 |
| 切换开销 | 大(切换页表、刷新 TLB、保存/恢复寄存器等) | 小(共享进程地址空间,仅需切换栈和寄存器) |
| 创建/销毁 | 开销大(分配内存、创建 PCB 等内核数据结构) | 开销小(仅分配栈和 TCB) |
| 通信方式 | IPC:管道(Pipe)、消息队列、共享内存、Socket、信号量 | 共享内存(堆中的对象)+ 需同步控制(锁、volatile) |
| 崩溃影响 | 一个进程崩溃不影响其他进程(地址空间隔离) | 一个线程的未处理异常可能导致整个 JVM 进程退出 |
| 多核利用 | 多进程可分布到多核 | 多线程可分布到多核(Java 线程本质是 OS 线程) |
操作系统层面的线程实现模型:
┌─────────────────────────────────────────────────┐
│ 1. 内核线程模型 (1:1) — Java 采用 │
│ 每个用户线程 → 一个内核线程(轻量级进程 LWP) │
│ 优点:一个线程阻塞不影响其他线程,充分利用多核 │
│ 缺点:创建/切换开销依赖内核,数量受限于内核资源 │
├─────────────────────────────────────────────────┤
│ 2. 用户线程模型 (N:1) — 早期 JVM(Green Thread)│
│ 多个用户线程 → 一个内核线程 │
│ 优点:无内核参与,切换极快 │
│ 缺点:一个线程阻塞,全部阻塞,无法利用多核 │
├─────────────────────────────────────────────────┤
│ 3. 混合线程模型 (N:M) — Go 协程、Project Loom │
│ 多个用户线程 ↔ 多个内核线程,动态映射 │
└─────────────────────────────────────────────────┘
Java 线程:HotSpot JVM 采用 1:1 内核线程模型,每个
java.lang.Thread对象对应一个操作系统线程。JDK 19+ 的 Project Loom 引入了虚拟线程(Virtual Thread),采用 N:M 模型。
线程上下文切换的具体过程:
- 挂起当前线程,保存 CPU 寄存器状态(PC、栈指针、通用寄存器)到 TCB
- 操作系统调度器选择下一个线程
- 恢复该线程的寄存器状态,更新 CPU 缓存
- 切回该线程代码执行
什么时候发生上下文切换?
- 时间片耗尽(抢占式调度)
- 线程主动让出 CPU:
sleep()、wait()、yield()、LockSupport.park() - 高优先级线程就绪
- 发生硬件中断
1.2 并发 vs 并行
并发 (Concurrency):多个任务在同一时间段内交替执行(单核 CPU 快速切换)
──▲── ▲─ ▲──▲─ ▲ ──▲ → 时间轴
T1 T2 T1 T2 T1
并行 (Parallelism):多个任务在同一时刻真正同时执行(多核 CPU)
T1 ──────────────→ 核 1
T2 ──────────────→ 核 2
| 对比维度 | 并发 | 并行 |
|---|---|---|
| 核心含义 | 逻辑上的同时发生 | 物理上的真正同时 |
| 执行方式 | 交替执行(单核或多核上快速切换) | 同时执行(必须多核) |
| 必要条件 | 代码结构支持(多线程编写) | 硬件支持(多核 CPU) |
| 关注点 | 任务间的调度与协调 | 任务间的分解与加速 |
| 反义词 | 串行(Sequential) | 串行(Sequential) |
| 生活中的例子 | 一个人同时聊多个微信 | 多个人各聊各的 |
| Java 体现 | 单核上多个线程竞争时间片 | 多核上多个线程真正同时运行 |
重要认知:并发是程序的结构特性(设计目标),并行是程序的执行特性(运行时行为)。多线程程序即使跑在单核 CPU 上也是并发的,但不一定是并行的。
1.3 线程的创建方式(5 种)
方式 1:继承 Thread 类
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running");
}
}
MyThread t = new MyThread();
t.start(); // ⚠️ 必须调用 start(),直接调用 run() 只是普通方法调用
为什么不推荐?
- Java 单继承限制:继承了 Thread 就不能继承其他类
- 任务和线程耦合:任务代码与线程控制混杂在一起
- 无法使用线程池复用
方式 2:实现 Runnable 接口
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("task running");
}
}
Thread t = new Thread(new MyTask());
t.start();
// Lambda 简写
new Thread(() -> System.out.println("running")).start();
优点:任务与线程分离,可以传给线程池,可继承其他类。优先使用。
方式 3:实现 Callable + FutureTask
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception { // 有返回值、可抛异常
Thread.sleep(1000);
return "done";
}
}
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask).start();
// get() 阻塞等待结果
String result = futureTask.get(); // 阻塞直到任务完成
String result2 = futureTask.get(500, TimeUnit.MILLISECONDS); // 超时等待
futureTask.cancel(true); // 尝试取消任务
boolean isCancelled = futureTask.isCancelled();
boolean isDone = futureTask.isDone();
FutureTask 内部状态转换:
NEW → COMPLETING → NORMAL (正常完成)
NEW → COMPLETING → EXCEPTIONAL (异常)
NEW → CANCELLED (被取消)
NEW → INTERRUPTING → INTERRUPTED (运行中被取消)
Runnable vs Callable 对比:
| 对比点 | Runnable | Callable |
|---|---|---|
| 方法签名 | void run() | V call() throws Exception |
| 返回值 | 无 | 有 |
| 异常 | 不能抛出受检异常,只能在内部 try-catch | 可以抛出受检异常 |
| 配合使用 | Thread、Executor.execute() | FutureTask、ExecutorService.submit() |
方式 4:通过 ExecutorService(线程池)
ExecutorService pool = Executors.newFixedThreadPool(10);
// 无返回值
pool.execute(() -> System.out.println("running"));
// 有返回值
Future<String> future = pool.submit(() -> {
Thread.sleep(100);
return "result";
});
String result = future.get(); // 阻塞获取
pool.shutdown(); // 优雅关闭,不接受新任务,执行完已有任务
pool.shutdownNow(); // 立即关闭,尝试中断正在执行的任务
方式 5:CompletableFuture(JDK8+,推荐异步编程)
// 不指定线程池,默认用 ForkJoinPool.commonPool()
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
return "hello";
});
// 指定自定义线程池
ExecutorService myPool = Executors.newFixedThreadPool(4);
CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> {
return "world";
}, myPool);
// 链式组合
String result = cf.thenCombine(cf2, (a, b) -> a + " " + b).join();
| 创建方式 | 返回值 | 异常抛出 | 配合线程池 | 推荐度 |
|---|---|---|---|---|
| 继承 Thread | 无 | ❌ | ❌ | ❌ |
| 实现 Runnable | 无 | ❌ | ✅ | ⭐⭐ |
| 实现 Callable | 有 | ✅ | ✅ | ⭐⭐⭐ |
| 线程池直接提交 | 通过 Future | 通过 Future | ✅ | ⭐⭐⭐⭐ |
| CompletableFuture | 有,链式 | ✅,exceptionally | 可选 | ⭐⭐⭐⭐⭐ |
start() vs run() 对比(⭐ 高频面试)
| 维度 | start() | run() |
|---|---|---|
| 作用 | 启动新线程,JVM 调用 native 方法创建 OS 线程 | 普通 Java 方法调用 |
| 执行线程 | 新线程中执行 run() | 当前线程(调用者线程)直接执行 |
| 调用次数 | 一个线程对象只能调用一次,多次抛 IllegalThreadStateException | 可多次调用(普通方法) |
| 并发 | 真正的多线程并发 | 串行执行,不产生新线程 |
Thread t = new Thread(() -> System.out.println(Thread.currentThread().getName()));
t.run(); // 输出 "main" — 当前线程执行!
t.start(); // 输出 "Thread-0" — 新线程执行!
底层原理:start() → native start0() → JVM 创建操作系统线程 → 新线程回调 run()。
1.4 线程生命周期(6 种状态详解)
注意区分:操作系统层面通常 5 种状态(New→Ready→Running→Blocked→Terminated),但 Java 将 Running 和 Ready 合并为 RUNNABLE,并将等待细分为 BLOCKED/WAITING/TIMED_WAITING,共 6 种状态。
┌─────────┐
│ NEW │ 线程创建但未调用 start()
│ │ 此时未分配 OS 资源
└────┬────┘
│ start() — 分配资源,等待 CPU 调度
┌────▼──────────────┐
┌─────│ RUNNABLE │ ←─┐
│ │ ① 就绪态:等待CPU │ │
│ │ ② 运行态:CPU执行 │ │
│ └──┬──────┬────┬───┘ │
│ │ │ │ │
synchronized │ Object.wait() │ │
等锁(block) │ Thread.join() │ │
│ │ LockSupport │ │
│ │ .park() │ │
│ │ │ │ │
┌────▼────┐ │ ┌────▼───────┐ │ │
│ BLOCKED │ │ │ WAITING │ │ │
│等待进入 │ │ │ 无限等待 │ │ │
│synchro- │ │ │ 必须被显式 │ │ │
│nized块 │ │ │ 唤醒 │ │ │
└────┬────┘ │ └────┬───────┘ │ │
│ │ │ │ │
│ notify/notifyAll │ │
│ unpark/interrupt │ │
│ │ │ │ │
拿到锁 └──────┼─────────┘ │
│ │ │
│ ┌──────▼──────────┐ │
│ │ TIMED_WAITING │ │
│ │ 超时等待 │ │
│ │ sleep(time), │ │
│ │ wait(time), │ │
│ │ join(time) │ │
│ └──────┬──────────┘ │
│ │ timeout/ │
│ │ notify/ │
│ │ interrupt │
│ │ │
└─────────────┼────────────────┘
│ run() 执行完毕
│ 或 未捕获异常
┌───────▼──────┐
│ TERMINATED │
│ 线程终止 │
└──────────────┘
6 种状态详细说明:
| 状态 | 含义 | 进入方式 | 退出方式 |
|---|---|---|---|
| NEW | 线程对象创建,start() 尚未调用 | new Thread() | start() 进入 RUNNABLE |
| RUNNABLE | JVM 层面的可运行态(含 OS 的 Running + Ready) | start()、从 WAITING/TIMED_WAITING/BLOCKED 被唤醒 | yield() 让出 CPU(仍为 RUNNABLE) |
| BLOCKED | 等待进入 synchronized 代码块/方法时被阻塞 | 尝试获取已被其他线程持有的 monitor 锁 | 拿到锁,进入 RUNNABLE |
| WAITING | 无限期等待其他线程显式唤醒 | Object.wait()、Thread.join()、LockSupport.park() | notify()/notifyAll()、unpark()、interrupt() |
| TIMED_WAITING | 带超时的等待 | Thread.sleep(ms)、Object.wait(ms)、Thread.join(ms)、LockSupport.parkNanos(ms) | 超时到期 / notify() / unpark() |
| TERMINATED | 线程执行完毕 | run() 正常执行完成 / 未捕获异常退出 | 不可恢复 |
关键区分:
- BLOCKED:被动等待,等别的线程释放锁。只针对
synchronized。 - WAITING:主动等待,等别的线程通知唤醒。
ReentrantLock.lock()内部使用LockSupport.park(),线程状态也是 WAITING。 - TIMED_WAITING:WAITING 的超时版本,到时间会自动唤醒。
yield() 与线程状态:
Thread.yield()是一个暗示(hint),告诉调度器当前线程愿意让出 CPU- 不会释放锁
- 状态不改变,从 RUNNABLE(Running)回到 RUNNABLE(Ready),可能立即被调度器再次选中
- 实际效果依赖于操作系统调度器,不可靠
线程中断机制:
// interrupt() — 设置中断标志位
thread.interrupt();
// 三种响应方式:
// 1. 当前线程检查自己的中断状态
while (!Thread.currentThread().isInterrupted()) {
// 干活...
}
// 2. 阻塞方法(sleep/wait/join)抛出 InterruptedException
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 中断标志已被清除,需要时重新设置
Thread.currentThread().interrupt(); // 恢复中断标志
}
// 3. LockSupport.park() 被中断时立即返回(不清除中断标志)
中断方法对比:
| 方法 | 清除中断标志 | 说明 |
|---|---|---|
isInterrupted() | ❌ 不清除 | 普通检查 |
interrupted() | ✅ 清除 | 静态方法,调用的是当前线程,检查后清除标志 |
interrupt() | — | 设置中断标志 |
二、Java 内存模型(JMM)
2.1 为什么需要 JMM?
在没有 JMM 的情况下,多线程程序的执行结果取决于:
- CPU 缓存:每个核心有自己的 L1/L2 缓存,写操作不会立即同步到主内存
- 指令重排序:编译器和 CPU 为了性能会打乱指令执行顺序
JMM 定义了一套规范,屏蔽了不同硬件/操作系统的内存访问差异,确保 Java 程序在各种平台上有一致的行为。
2.2 JMM 核心概念
┌───────────────────┐ ┌───────────────────┐
│ 线程 A │ │ 线程 B │
│ │ │ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ 本地内存 A │ │ │ │ 本地内存 B │ │
│ │ (CPU 寄存器 │ │ │ │ (CPU 寄存器 │ │
│ │ + L1/L2缓存)│ │ │ │ + L1/L2缓存)│ │
│ │ │ │ │ │ │ │
│ │ x = 1 ←┐ │ │ │ │ read x → 1 │ │
│ └──────┬────┘ │ │ │ └──────┬────┘ │ │
└─────────┼───────┘ │ └─────────┼───────┘ │
│ flush │ │ load │
▼ │ │ │
┌──────────────────────────────────────────────────┐ │
│ 主内存 (Main Memory) │ │
│ 所有线程共享的变量,与堆内存对应 │ │
│ │ │
│ x = 0 ————> x = 1 │ │
└──────────────────────────────────────────────────┘
JMM 规定的关键操作(8 个原子操作):
| 操作 | 作用域 | 含义 |
|---|---|---|
lock | 主内存 | 将变量标识为某线程独占 |
unlock | 主内存 | 释放独占的变量 |
read | 主内存 | 从主内存读取到工作内存 |
load | 工作内存 | 将 read 到的值放入工作内存变量副本 |
use | 工作内存 | 将工作内存变量值传给执行引擎 |
assign | 工作内存 | 将执行引擎收到的值赋给工作内存变量 |
store | 工作内存 | 将工作内存变量值传送到主内存 |
write | 主内存 | 将 store 过来的值写入主内存变量 |
必须遵守的规则:
read和load、store和write必须成对出现- 不允许线程丢弃最近的
assign(赋值后必须刷回主内存) - 工作内存中的变量在没有
assign的情况下不允许无原因刷回主内存
2.3 指令重排序(3 种类型)
// 源代码
int a = 1; // (1)
int b = 2; // (2)
int c = a + b; // (3)
| 重排序类型 | 发生阶段 | 说明 |
|---|---|---|
| 编译器重排序 | 编译期 | JIT 编译器在保证单线程语义的前提下重新安排语句顺序 |
| 指令级并行重排序 | CPU 执行期 | CPU 流水线优化,无数据依赖的指令可并行/提前执行 |
| 内存系统重排序 | 写入/读取时 | 由于缓存/写缓冲区的存在,写操作对不同核心可见的顺序可能不同 |
as-if-serial 语义:无论怎么重排序,单线程程序的执行结果不能被改变。编译器、CPU 都必须遵守。但多线程环境下,重排序的效果会对其他线程可见,导致并发问题。
经典重排序导致的问题 — DCL 单例:
public class Singleton {
private static Singleton instance; // ⚠️ 如果不用 volatile
public static Singleton getInstance() {
if (instance == null) { // (1) 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // (2) 第二次检查
instance = new Singleton(); // (3) 关键行!
// new Singleton() 底层三步:
// A. 分配内存空间
// B. 执行构造函数初始化对象
// C. 将引用指向分配的内存地址
// ⚠️ 重排序后可能变成 A→C→B!
// 如果线程1执行完 A→C 后被挂起,
// 线程2 在 (1) 处看到 instance != null,
// 但拿到的是一个未初始化完成的对象!
}
}
}
return instance;
}
}
// 解决方案:instance 加 volatile 禁止指令重排序
private static volatile Singleton instance;
2.4 happens-before 规则详解(JMM 核心)
JMM 通过 happens-before 规则来规定两个操作之间的内存可见性:如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见,且 A 的执行顺序在 B 之前。
8 条规则逐条解析:
① 程序次序规则(Program Order Rule)
同一线程内,书写在前面的代码 happens-before 书写在后面的代码。
int a = 1; // hb
int b = 2; // 本句
注意:这里说的是"控制流顺序"而非"程序代码顺序",在有分支/循环时以实际执行为准。
② volatile 变量规则(Volatile Variable Rule)
对 volatile 变量的写操作 happens-before 后续对这个 volatile 变量的读操作。
线程 A: volatileVar = 1; // write
线程 B: int x = volatileVar; // read — 看到的一定是 1
③ 传递性(Transitivity)
若 A hb B,B hb C,则 A hb C。
线程 A: normalVar = 10; // (A)
volatileVar = 1; // (B)
线程 B: if (volatileVar == 1) { // (C) 看到 B
// 这里一定看到 normalVar == 10 ← 通过 A→B→C 传递
}
④ 锁规则(Monitor Lock Rule)
对同一个锁的解锁 happens-before 后续对这个锁的加锁。
线程 A: synchronized(lock) { x = 10; } // 解锁 hb
线程 B: synchronized(lock) { /* 一定看到 x == 10 */ }
⑤ 线程 start() 规则
thread.start() happens-before 该线程内的所有操作。
主线程: thread.start(); // hb
新线程: thread.run() 内的第一条语句
⑥ 线程 join() 规则
线程内的所有操作 happens-before thread.join() 成功返回。
线程 A: count = 100; // hb
主线程: threadA.join(); // 之后一定看到 count == 100
⑦ 线程中断规则
对线程 interrupt() 的调用 happens-before 被中断线程检测到中断事件。
主线程: thread.interrupt(); // hb
线程 B: thread.isInterrupted() == true
⑧ 对象终结规则
构造函数的结束 happens-before finalize() 方法的开始。
2.5 并发编程三大特性
线程安全
│
┌────────────┼────────────┐
▼ ▼ ▼
原子性 可见性 有序性
(Atomicity) (Visibility) (Ordering)
原子性
| 操作 | 是否原子 |
|---|---|
int a = 5; 基本类型赋值(除 long/double 在 32 位 JVM) | ✅ |
int b = a; 读 + 赋值 | ❌ 两步 |
a++; 读 + 改 + 写 | ❌ 三步 |
使用 AtomicInteger CAS 操作 | ✅ |
synchronized 块内的操作 | ✅ |
32 位 JVM 上 long/double 的读写 | ❌ 可能分两次 32 位操作 |
保证原子性的手段:synchronized、ReentrantLock、AtomicXXX(CAS)
可见性
// 无 volatile:线程 B 可能永远看不到线程 A 的修改
class VisibilityProblem {
private boolean flag = false; // ⚠️ 不加 volatile
void writer() { flag = true; }
void reader() {
while (!flag) { /* JIT 可能将 flag 缓存在寄存器,死循环 */ }
System.out.println("done");
}
}
JIT 编译器导致的可见性问题:
- 逃逸分析:编译器认为
flag在当前线程内不会被修改,将其缓存到寄存器 - 循环提升:将
while (!flag)优化为if (!flag) { while(true) {} }
保证可见性的手段:volatile、synchronized、Lock、final
synchronized 如何保证可见性? JMM 规定:
- 线程加锁前,必须清空工作内存,从主内存重新加载
- 线程解锁前,必须把工作内存中的修改刷新到主内存
有序性
保证有序性的手段:volatile、synchronized、Lock
volatile 如何禁止重排序? 内存屏障(Memory Barrier):
┌──────────────────────────────────────────────┐
│ volatile 写之前插入 StoreStore 屏障 │
│ 确保之前所有普通写对 volatile 写可见 │
│ volatile 写之后插入 StoreLoad 屏障 │
│ 确保 volatile 写不会被重排到后续读写之后 │
├──────────────────────────────────────────────┤
│ volatile 读之后插入 LoadLoad 屏障 │
│ 确保后续读不会被重排到 volatile 读之前 │
│ volatile 读之后插入 LoadStore 屏障 │
│ 确保后续写不会被重排到 volatile 读之前 │
└──────────────────────────────────────────────┘
四种屏障详解:
| 屏障类型 | 指令 | 作用 |
|---|---|---|
| LoadLoad | Load1; LoadLoad; Load2 | 保证 Load1 的数据加载在 Load2 之前完成 |
| StoreStore | Store1; StoreStore; Store2 | 保证 Store1 的写入对后续 Store 可见 |
| LoadStore | Load1; LoadStore; Store2 | 保证 Load1 在 Store2 的写入之前完成 |
| StoreLoad | Store1; StoreLoad; Load2 | 保证 Store1 对所有处理器可见后才执行 Load2。最重,杀所有重排序 |
2.6 volatile 详解
volatile 三不保证 & 应用场景
volatile 保证:可见性 ✅、有序性 ✅
volatile 不保证:原子性 ❌
volatile int count = 0;
// 场景 1:原子的 volatile 操作 ✅
count = 1; // 写是原子的
int x = count; // 读是原子的
// 场景 2:非原子的复合操作 ❌
count++; // 读-改-写,不是原子的
// 线程 A 读 count=0,线程 B 读 count=0
// 各自 +1 写回 =1,丢失一次更新
volatile 适用场景(必须同时满足):
| 条件 | 说明 |
|---|---|
| ✅ 对变量的写不依赖当前值 | flag = true 而非 count++ |
| ✅ 变量不需要与其他状态变量共同参与不可分割的操作 | — |
典型适用:
- 状态标志量:
volatile boolean initialized - DCL 单例模式的 instance 引用
- 独立观察值(单次读就获取完全正确的值)
典型不适用:
i++、i = i + 1等复合操作- 需要保证多个变量操作原子性的场景
volatile 与 DCL 单例完整解析
public class Singleton {
// volatile 关键作用:
// 1. 禁止指令重排(A→C→B 变为 A→B→C)
// 2. 保证多线程间的可见性
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查(无锁,性能优化)
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查(有锁,安全保证)
// 三步:分配内存 → 初始化 → 引用赋值
// volatile 禁止这三步重排序
instance = new Singleton();
}
}
}
return instance;
}
}
2.7 synchronized 详解
三种使用方式
// 方式 1:同步代码块 — 锁是括号内的对象
Object lock = new Object();
synchronized (lock) {
// 临界区
}
// 方式 2:同步实例方法 — 锁是 this(当前实例对象)
public synchronized void method() {
// 相当于 synchronized(this) { ... }
}
// 方式 3:同步静态方法 — 锁是类的 Class 对象
public static synchronized void staticMethod() {
// 相当于 synchronized(MyClass.class) { ... }
}
实例方法同步 vs 静态方法同步:互不干扰!一个是锁 this 对象,一个是锁 Class 对象,两把不同的锁。
字节码层面原理
public void syncBlock() {
synchronized (this) {
System.out.println("Hello");
}
}
对应字节码:
0: aload_0
1: dup
2: astore_1
3: monitorenter ← 获取 monitor 锁
4: getstatic ...
7: ldc #...
9: invokevirtual ...
12: aload_1
13: monitorexit ← 正常释放 monitor 锁
14: goto 22
17: astore_2
18: aload_1
19: monitorexit ← 异常路径也释放 monitor 锁
20: aload_2
21: athrow
22: return
关键细节:编译器会自动在异常处理表中加入
monitorexit,保证即使发生异常锁也能正常释放。这是 synchronized 比 Lock 更安全的一个体现。
Monitor(管程)原理
┌──────────────────────────────────────────────┐
│ Object Monitor │
│ │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
│ │ _owner │ │ _EntryList│ │ _WaitSet │ │
│ │ null │ │ (竞争队列) │ │ (等待队列) │ │
│ │ │ │ thread T2 │ │ thread T3 │ │
│ │ │ │ thread T4 │ │ │ │
│ └─────────┘ └──────────┘ └──────────┘ │
│ │
│ owner = null → 可以竞争 │
│ owner = T1 → T2/T4 在 EntryList 中等待 │
│ wait() → owner 进入 WaitSet 释放锁 │
│ notify() → WaitSet 中的线程移到 EntryList │
└──────────────────────────────────────────────┘
ObjectMonitor 核心字段(C++ 源码精简):
ObjectMonitor() {
_header = NULL; // 对象头 Mark Word
_count = 0; // 重入计数
_waiters = 0; // 等待线程数
_recursions = 0; // 重入次数
_object = NULL; // 关联的 Java 对象
_owner = NULL; // 持有锁的线程
_EntryList = NULL; // 阻塞等待获取锁的线程链表
_WaitSet = NULL; // 调用了 wait() 的线程链表
}
锁升级(膨胀)过程详解
锁升级方向不可逆:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。只能升级不能降级(偏向锁可撤销,重置为无锁)。
时间线
────────────────────────────────────────────────►
阶段 1: 无锁
[对象头 Mark Word 最后 3 位 = 001]
阶段 2: 偏向锁 (JDK6 默认开启,JDK15 默认关闭,JDK21 彻底废弃)
条件:同一线程反复获取同一把锁
实现:Mark Word 存储线程 ID
获取:
检查 Mark Word 的线程 ID == 当前线程?
→ 是:直接获取(无需 CAS)
→ 否:CAS 尝试修改线程 ID
→ 成功:获取偏向锁
→ 失败:撤销偏向锁,升级轻量级锁
重偏向:偏向锁撤销达到阈值(-XX:BiasedLockingBulkRebiasThreshold=20)时
触发批量重偏向
阶段 3: 轻量级锁
条件:多线程"交替"执行同步块,不存在实际竞争
实现:通过 CAS 自旋获取
过程:
1. 当前线程栈帧中创建 Lock Record
2. 将对象头 Mark Word 拷贝到 Lock Record
3. CAS 尝试将对象头 Mark Word 替换为指向 Lock Record 的指针
→ 成功:获取轻量级锁,Mark Word 末 2 位 = 00
→ 失败:自旋一定次数
→ 一直失败(竞争激烈):膨胀为重量级锁
阶段 4: 重量级锁
条件:多线程同时竞争同一把锁
实现:操作系统 Mutex Lock
过程:
1. 向 OS 申请互斥量
2. 对象头 Mark Word 存储指向 ObjectMonitor(堆中)的指针
3. 未获取锁的线程 → 进入 EntryList → BLOCKED 状态
4. 锁释放时从 EntryList 中挑选一个线程唤醒
Mark Word 在不同锁状态下的存储结构(64 位 JVM):
| 锁状态 | 存储内容 | 锁标志位 |
|---|---|---|
| 无锁 | unused(25) | hashcode(31) | unused(1) | age(4) | biased_lock(1) | 01 | 001 |
| 偏向锁 | threadID(54) | epoch(2) | unused(1) | age(4) | biased_lock(1) | 01 | 101 |
| 轻量级锁 | ptr_to_lock_record(62) | 00 | 00 |
| 重量级锁 | ptr_to_object_monitor(62) | 10 | 10 |
| GC 标记 | — | 11 |
synchronized 优化(JDK6+)
| 优化技术 | 说明 |
|---|---|
| 偏向锁 | 单线程重复获取时,无 CAS 开销 |
| 轻量级锁 | 无实际竞争时,CAS 自旋避免阻塞 |
| 自旋锁 | 不挂起线程,空转等待,适合锁持有时间短的场景 |
| 自适应自旋 | JVM 根据前次自旋成功率自动调整自旋次数 |
| 锁消除 | JIT 分析无共享变量逃逸,直接去掉同步 |
| 锁粗化 | 连续加锁解锁合并为一次更大的锁,减少加锁次数 |
锁消除示例:
// 使用 StringBuffer 的方法,内部 append 都有 synchronized
// 但 sb 是局部变量,不会逃逸,JIT 会消除掉所有 synchronized
public String concat(String a, String b) {
StringBuffer sb = new StringBuffer();
sb.append(a); // synchronized 会被消除
sb.append(b); // synchronized 会被消除
return sb.toString();
}
锁粗化示例:
// 循环内反复加锁解锁
for (int i = 0; i < 100; i++) {
synchronized (lock) { doSomething(); }
}
// JIT 优化为(如果条件合适)
synchronized (lock) {
for (int i = 0; i < 100; i++) { doSomething(); }
}
2.8 CAS 详解
CAS 原理
// CAS 由 CPU 指令 cmpxchg 保证原子性
// Unsafe 类提供 CAS 原语(只能被 JDK 内部类通过反射调用)
public final class Unsafe {
public final native boolean compareAndSwapInt(
Object o, // 对象
long offset, // 字段在对象中的偏移量
int expected, // 期望值
int x // 新值
);
}
CPU 层面:cmpxchg 指令前加 lock 前缀(lock cmpxchg),锁定总线或缓存行,保证原子性。
自旋获取:
// AtomicInteger.incrementAndGet() 的 CAS 自旋逻辑
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset); // 读当前值
} while (!compareAndSwapInt(o, offset, v, v + delta)); // CAS 失败则重试
return v;
}
ABA 问题详解
时间线:
T1: 读到值 A
T2: A → B → A (先改成B,又改回A)
T1: CAS(A, C) → 成功!但中间经历了 A→B→A 的变化,T1 不知道
示例代码:
// 有 ABA 问题的情况
AtomicInteger ai = new AtomicInteger(100);
// 线程1: 预期 100,改为 50
// 线程2: 先改为 20,再改为 100 (ABA)
// 线程1 的 CAS(100, 50) 仍成功,但中间已发生过变化
// 解决方案1: AtomicStampedReference 带版本号
AtomicStampedReference<Integer> asr =
new AtomicStampedReference<>(100, 0);
int stamp = asr.getStamp();
asr.compareAndSet(100, 50, stamp, stamp + 1);
// 解决方案2: AtomicMarkableReference 带布尔标记
AtomicMarkableReference<Integer> amr =
new AtomicMarkableReference<>(100, false);
amr.compareAndSet(100, 50, false, true);
原子类体系
┌─────────────────────┐
│ Atomic 家族 │
└─────────┬───────────┘
┌──────────────────┼──────────────────┐
▼ ▼ ▼
标量原子类 原子数组类 原子引用类
AtomicInteger AtomicIntegerArray AtomicReference
AtomicLong AtomicLongArray AtomicStampedReference
AtomicBoolean AtomicMarkableReference
│
▼
累加器 (JDK8+)
LongAdder ← 高并发下比 AtomicLong 吞吐更高
DoubleAdder 原理:分散热点,Cell[] 分段累加
LongAccumulator
DoubleAccumulator
LongAdder vs AtomicLong
// AtomicLong: 单热点,高并发 CAS 自旋严重
// 线程1 ─┐
// 线程2 ─┼→ CAS(base, base+1) ← 所有线程竞争一个变量
// 线程3 ─┘
// LongAdder: 分散热点 (JDK8+)
// 线程1 → Cell[0].value++
// 线程2 → Cell[1].value++ ← 每个线程落到不同 Cell
// 线程3 → Cell[2].value++ 最终 sum = base + ΣCell[i]
// Cell 数组由 @Contended 注解填充避免伪共享
LongAdder 适用场景:
- 高并发下统计计数(如 QPS 统计、访问计数)
- 需要最终一致性但不需要实时精确值
sum()方法不是原子的(求和时可能有并发更新)
三、锁机制深度解析
3.1 Lock 接口体系
Lock (接口)
│
├── ReentrantLock 可重入互斥锁
│
├── ReentrantReadWriteLock.ReadLock 读锁(共享)
├── ReentrantReadWriteLock.WriteLock 写锁(排他)
│
└── StampedLock (JDK8)
├── 悲观读锁
├── 悲观写锁
└── 乐观读锁
ReentrantLock 特性
public class ReentrantLock implements Lock, Serializable {
// 内部同步器,基于 AQS
private final Sync sync;
// 抽象基类,继承 AQS
abstract static class Sync extends AbstractQueuedSynchronizer { ... }
// 非公平实现(默认)
static final class NonfairSync extends Sync { ... }
// 公平实现
static final class FairSync extends Sync { ... }
}
重入原理:
// 简化版实现
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState(); // 当前 AQS state
if (c == 0) { // 无锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// ⭐ 重入的关键:同一个线程,state 累加
int nextc = c + acquires;
setState(nextc);
return true;
}
return false;
}
// 释放时同理,state 累减,减到 0 才真正释放
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // 完全释放
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
公平锁 vs 非公平锁核心区别:
// 非公平 NonfairSync.tryAcquire:
// 直接 CAS 抢锁,不管有没有人排队
final boolean tryAcquire(int acquires) {
if (getState() == 0) {
if (compareAndSetState(0, acquires)) { // 直接抢
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
}
// 重入逻辑同上...
}
// 公平 FairSync.tryAcquire:
// 先看有没有人排队,有人排队就不抢
final boolean tryAcquire(int acquires) {
if (getState() == 0) {
if (!hasQueuedPredecessors() && // ⭐ 先检查是否有人排在前面
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
}
// 重入逻辑同上...
}
非公平锁吞吐量更高的原因:一个新线程来直接 CAS 抢锁,如果恰好锁刚释放,抢到后直接执行,避免了唤醒阻塞线程的上下文切换开销。
使用建议:
- 吞吐量优先 → 非公平锁(默认)
- 公平性优先 → 公平锁(避免线程饥饿)
3.2 synchronized vs ReentrantLock 深度对比
| 对比维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层级 | JVM 层面,C++ 实现 | JDK 层面,Java 实现(基于 AQS) |
| 获取与释放 | 隐式,进入代码块获取,退出代码块释放 | 显式,lock() 和 unlock() |
| 异常处理 | 自动释放,不用担心忘记释放 | 必须在 finally 中手动释放 |
| 可中断 | ❌ 等待锁时不可中断 | ✅ lockInterruptibly() |
| 超时获取 | ❌ 不支持 | ✅ tryLock(timeout, unit) |
| 非阻塞尝试 | ❌ 不支持 | ✅ tryLock() 立即返回 |
| 公平锁 | ❌ 只支持非公平 | ✅ 可选公平/非公平 |
| 多条件等待 | ❌ 单个隐式条件(wait/notify) | ✅ 多个 Condition 对象 |
| 持有线程查询 | ❌ 无法知道哪个线程持有锁 | ✅ isHeldByCurrentThread() getOwner() |
| 等待线程查询 | ❌ 无法知道有多少线程在等待 | ✅ getQueueLength() hasQueuedThreads() |
| 性能 | JDK6+ 优化后差距很小 | 高竞争场景略优 |
| 锁绑定 | 代码块级 | 可跨方法(lock 和 unlock 在不同方法中调用) |
什么时候选 Lock?
- 需要超时获取锁(如数据库连接池,等 3 秒拿不到就降级)
- 需要可中断地获取锁(如任务可以被取消)
- 需要多个条件变量(如生产者-消费者,需要用不同 Condition 区分"满了"和"空了")
- 需要非阻塞的 tryLock(如抢红包,抢不到就算了)
3.3 AQS 深度解析
AQS(AbstractQueuedSynchronizer)是 JUC 大多数同步器的基石。
AQS 核心设计
public abstract class AbstractQueuedSynchronizer {
// ═══════════ volatile + CAS 保证线程安全 ═══════════
private volatile int state;
protected final int getState() { return state; }
protected final void setState(int newState) { state = newState; }
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
// ═══════════ CLH 变体队列 ═══════════
// 双向链表,保存等待线程
private transient volatile Node head;
private transient volatile Node tail;
static final class Node {
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 等待的线程
volatile int waitStatus; // 当前节点状态
Node nextWaiter; // 条件队列的下一个节点
// waitStatus 取值:
// CANCELLED(1) — 超时/中断已取消
// SIGNAL(-1) — 后继节点需要被唤醒
// CONDITION(-2) — 节点在条件队列中
// PROPAGATE(-3) — 共享模式下传播释放
// 0 — 默认值
}
// ═══════════ 两种模式 ═══════════
// 排他模式:同一时刻只有一个线程持有锁
// 共享模式:多个线程可同时持有锁
static final Node EXCLUSIVE = null; // 排他模式标记
static final Node SHARED = new Node(); // 共享模式标记(哨兵)
}
state 的两种语义
| 同步器 | state 含义 |
|---|---|
ReentrantLock | 0=未锁定, 1=锁定, N>1=重入 N 次 |
ReentrantReadWriteLock | 高 16 位=读锁持有数, 低 16 位=写锁重入次数 |
Semaphore | 剩余许可证数量 |
CountDownLatch | 还需 countDown 的次数,到 0 就释放 |
AQS 排他锁获取流程(acquire 源码分析)
// 排他锁获取入口
public final void acquire(int arg) {
// 1. tryAcquire → 由子类实现,返回 true 表示成功
// 2. 失败则 addWaiter → 封装成 Node 加入 CLH 队列尾部
// 3. acquireQueued → 在队列中自旋等待
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt(); // 如果因中断醒来,恢复中断标志
}
// 加入等待队列
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) { // 快速路径:CAS 入队尾
pred.next = node;
return node;
}
}
enq(node); // 慢路径:自旋 CAS 入队
return node;
}
// 在队列中自旋等待
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 只有前驱是 head 才有资格 tryAcquire
if (p == head && tryAcquire(arg)) {
setHead(node); // 获取成功,设为新 head
p.next = null; // 帮助 GC
failed = false;
return interrupted;
}
// 判断是否需要 park 当前线程
// shouldParkAfterFailedAcquire: 将前驱 waitStatus 设为 SIGNAL
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // LockSupport.park(this)
interrupted = true;
}
} finally {
if (failed) cancelAcquire(node);
}
}
AQS 排他锁释放流程
public final boolean release(int arg) {
if (tryRelease(arg)) { // 子类实现,state 归零
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒 head 的下一个节点
return true;
}
return false;
}
AQS 共享模式
// 共享获取(Semaphore、CountDownLatch、读写锁的读锁)
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) // < 0 表示获取失败
doAcquireShared(arg); // 入队自旋等待
}
// 共享释放 — 关键不同:释放后会传播唤醒,连续唤醒多个等待的共享节点
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared(); // 传播释放,逐级唤醒
return true;
}
return false;
}
Condition 条件队列
// synchronized 只有单一条件队列(WaitSet)
// Lock 可以有多个 Condition,每个 Condition 维护自己的等待队列
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition(); // "不满"条件
final Condition notEmpty = lock.newCondition(); // "不空"条件
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // 满了,在 notFull 条件上等待
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal(); // 放入后,通知 notEmpty 条件
} finally { lock.unlock(); }
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await(); // 空了,在 notEmpty 条件上等待
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal(); // 取出后,通知 notFull 条件
return x;
} finally { lock.unlock(); }
}
}
Condition 与 Object monitor 方法的对应:
| Object Monitor | Condition | 说明 |
|---|---|---|
wait() | await() | 等待 |
wait(timeout) | await(time, unit) | 超时等待 |
notify() | signal() | 唤醒一个 |
notifyAll() | signalAll() | 唤醒全部 |
Condition 底层原理:每个 Condition 内部维护一个单向链表的条件等待队列。await() 时,节点从 AQS 的 CLH 同步队列转移到 Condition 的条件队列;signal() 时,节点从条件队列转移回 CLH 同步队列的队尾,等待重新获取锁。
3.4 读写锁深度解析
ReentrantReadWriteLock 核心规则
| 读锁(共享) | 写锁(排他) | |
|---|---|---|
| 读锁 | ✅ 多个读线程可同时持有读锁 | ❌ 有写锁时不能获取读锁 |
| 写锁 | ❌ 有读锁时不能获取写锁 | ❌ 只能一个写线程持有写锁 |
state 的分割使用
// ReentrantReadWriteLock 将 state 高 16 位给读锁,低 16 位给写锁
// state = (sharedCount << 16) | exclusiveCount
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << 16); // 65536
static final int EXCLUSIVE_MASK = (1 << 16) - 1; // 65535
static int sharedCount(int c) { return c >>> SHARED_SHIFT; } // 读锁持有数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } // 写锁重入次数
锁降级(允许)vs 锁升级(不允许)
// ✅ 锁降级:写锁 → 读锁(安全的,先获写锁再获读锁,再释放写锁)
public void cacheData() {
rwLock.writeLock().lock();
try {
// 1. 修改数据
data = fetchFromDB();
// 2. 获取读锁(此时还持有写锁)
rwLock.readLock().lock();
} finally {
// 3. 释放写锁
rwLock.writeLock().unlock();
}
// 4. 此时只持有读锁,可用读锁读取数据
try {
use(data); // 保证数据不被其他写锁修改
} finally {
rwLock.readLock().unlock();
}
}
// 锁降级的意义:在写锁释放后继续持有读锁,
// 防止其他线程在"写完成→读之前"这个窗口期获取写锁修改数据
// ❌ 锁升级:读锁 → 写锁(不允许,会造成死锁)
// 如果两个读线程同时尝试升级,都会等对方释放读锁 → 死锁
StampedLock(JDK8+)
StampedLock sl = new StampedLock();
// 1. 乐观读(无锁,性能最高)
long stamp = sl.tryOptimisticRead();
// 读取共享变量
if (!sl.validate(stamp)) { // 验证期间是否有写操作
stamp = sl.readLock(); // 降级到悲观读锁
try {
// 重新读取
} finally {
sl.unlockRead(stamp);
}
}
// 2. 悲观读锁
long stamp = sl.readLock();
try { /* 读 */ } finally { sl.unlockRead(stamp); }
// 3. 写锁
long stamp = sl.writeLock();
try { /* 写 */ } finally { sl.unlockWrite(stamp); }
| 对比 | ReentrantReadWriteLock | StampedLock |
|---|---|---|
| 可重入 | ✅ | ❌ 不可重入 |
| 锁模式 | 读/写 | 乐观读/悲观读/写 |
| Condition | ✅ | ❌ |
| 性能 | 中等 | 更高(乐观读无锁) |
| 使用难度 | 简单 | 复杂(注意 stamp 校验) |
读写锁的"写饥饿"问题
现象:大量读线程持续持有读锁,写线程永远拿不到写锁,导致写操作饿死。
原因:读锁是共享的,只要有一个读线程不释放读锁,即使有其他读线程在排队,写线程也必须等所有读锁释放。
如何避免:
- 使用 公平模式:
new ReentrantReadWriteLock(true),按排队顺序,读等写、写等读 - 使用 StampedLock 乐观读:乐观读不阻塞写(推荐)
- 设置读锁超时,定期让路
3.5 死锁深度分析
死锁的四个必要条件
| 条件 | 含义 | 破坏方式 |
|---|---|---|
| 互斥条件 | 资源一次只能被一个线程使用 | 无法破坏(锁的本质) |
| 持有并等待 | 持有资源 A,同时在等待资源 B | 一次性申请所有资源,不阻塞在持有状态下等待 |
| 不可剥夺 | 已获得的资源不能被强制夺取 | 用 tryLock 超时释放已有资源 |
| 循环等待 | 线程间形成头尾相连的等待环路 | 固定加锁顺序(最常用) |
死锁编码示例
public class DeadLockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
// 线程1:先 lockA 后 lockB
new Thread(() -> {
synchronized (lockA) {
System.out.println("T1 拿到 A");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) {
System.out.println("T1 拿到 B");
}
}
}, "T1").start();
// 线程2:先 lockB 后 lockA ← 顺序相反,与 T1 形成环路
new Thread(() -> {
synchronized (lockB) {
System.out.println("T2 拿到 B");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockA) {
System.out.println("T2 拿到 A");
}
}
}, "T2").start();
}
}
// 输出:T1 拿到 A / T2 拿到 B → 死锁,程序卡住
死锁排查
# 步骤1:找到 Java 进程
jps -l
# 输出:12345 DeadLockDemo
# 步骤2:打印线程堆栈
jstack 12345
# 输出末尾:
# Found one Java-level deadlock:
# =============================
# "T1":
# waiting to lock monitor 0x00007f... (lockB)
# - locked <0x00007f...> (lockA)
# "T2":
# waiting to lock monitor 0x00007f... (lockA)
# - locked <0x00007f...> (lockB)
# 步骤3(高级):用 jstack -m 混合模式查看,可以看到守护线程和 native 线程
jstack -m 12345
其他排查工具:
JConsole→ 线程 → 检测死锁VisualVM→ Thread Dump → 自动检测死锁- Arthas:
thread -b一键找出阻塞其他线程的罪魁祸首 - 代码中
ThreadMXBean.findDeadlockedThreads()
预防死锁的实践方法
// 方法1:固定加锁顺序(最推荐)
// 所有线程都按 lockA → lockB 的顺序获取
// 方法2:tryLock 超时放弃
public boolean transfer(Account from, Account to, int amount) {
while (true) {
if (from.lock.tryLock()) {
try {
if (to.lock.tryLock()) {
try {
from.debit(amount);
to.credit(amount);
return true;
} finally { to.lock.unlock(); }
}
} finally { from.lock.unlock(); }
}
// 两个锁没同时拿到时,休眠随机时间后重试
Thread.sleep(random.nextInt(100));
}
}
// 方法3:用锁的哈希值统一排序
if (System.identityHashCode(lockA) < System.identityHashCode(lockB)) {
synchronized (lockA) {
synchronized (lockB) { /* ... */ }
}
} else if (System.identityHashCode(lockA) > System.identityHashCode(lockB)) {
synchronized (lockB) {
synchronized (lockA) { /* ... */ }
}
} else {
// 哈希冲突的处理:引入第三个 tie-breaking 锁
synchronized (tieLock) {
synchronized (lockA) {
synchronized (lockB) { /* ... */ }
}
}
}
3.6 ThreadLocal 深度分析
数据结构全景
┌──────────────────────────────────────────────────────┐
│ Thread 对象 │
│ ┌──────────────────────────────────────────────┐ │
│ │ threadLocals (ThreadLocalMap) │ │
│ │ │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ Entry[] table (默认长度16) │ │ │
│ │ │ │ │ │
│ │ │ [0] → (TL@ref1 → value1) │ key 是弱引用 │ │
│ │ │ [1] → null │ │ │
│ │ │ [2] → (TL@ref2 → value2) │ │ │
│ │ │ [3] → null │ │ │
│ │ │ ... │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ │ │
│ │ Entry extends WeakReference<ThreadLocal<?>> │ │
│ │ Key: 弱引用 → ThreadLocal 对象 │ │
│ │ Value: 强引用 → 业务数据 │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ inheritableThreadLocals (可被子线程继承) │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
哈希冲突处理
ThreadLocalMap 使用开放地址法(线性探测)处理哈希冲突,而不是 HashMap 的链地址法。
// 哈希计算
int i = key.threadLocalHashCode & (table.length - 1);
// 如果 i 位置被占,则 i+1, i+2, i+3... 线性探测找到下一个空位
// ThreadLocal 的 hashCode 是递增的魔法数 0x61C88647(黄金分割数),
// 确保哈希分布均匀,减少冲突
set/get/remove 源码分析
// set()
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value; // 找到,更新 value
return;
}
if (k == null) {
replaceStaleEntry(key, value, i); // 清理过期的 Entry (key 被 GC 回收)
return;
}
}
tab[i] = new Entry(key, value); // 新位置
// ...
}
// get()
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // 线性探测查找
if (e != null) return (T) e.value;
}
return setInitialValue(); // 未找到,初始化默认值
}
内存泄漏详细分析
泄漏链路:
Thread 对象(强引用)
→ ThreadLocalMap(强引用)
→ Entry[](强引用)
→ Entry.Value(强引用)
→ Entry.Key(弱引用 → ThreadLocal 对象)
GC 发生时:ThreadLocal 对象因为没有外部强引用了,被回收
→ Entry.Key 变成 null
→ 但 Value 通过 Thread → ThreadLocalMap → Entry → Value 这条强引用链可达
→ Value 永远不会被回收
→ 只要 Thread 不销毁,Value 就一直占用内存
两种泄漏情况:
| 场景 | 线程类型 | 泄漏后果 |
|---|---|---|
| 线程池 | 线程复用,几乎不销毁 | 泄漏永久累积,OOM |
| 普通线程 | 任务完毕线程销毁 | 线程销毁时 ThreadLocalMap 回收,无泄漏 |
ThreadLocal 自身的防护措施:
get()/set()时会探测 key==null 的过期 Entry 并清除(启发式清理)remove()主动清除指定 Entry- 扩容时全量清理过期 Entry
最佳实践:
// 始终在 finally 中调用 remove()
ThreadLocal<MyData> tl = ThreadLocal.withInitial(MyData::new);
try {
MyData data = tl.get();
// ... 使用 data ...
} finally {
tl.remove(); // ⚠️ 必须清理!
}
InheritableThreadLocal
// 父线程创建的 ThreadLocal 值可以传递给子线程
InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();
itl.set("parent-value");
new Thread(() -> {
System.out.println(itl.get()); // "parent-value"
}).start();
原理:Thread 构造时,如果父线程的 inheritableThreadLocals 不为空,会复制一份给子线程。注意是浅拷贝,父线程和子线程指向同一个 Value 对象。
四、线程池深度解析
4.1 线程池核心设计
ThreadPoolExecutor 核心字段
public class ThreadPoolExecutor extends AbstractExecutorService {
// ctl 是一个 AtomicInteger,高 3 位存线程池状态,低 29 位存线程数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3; // 29
private static final int CAPACITY = (1 << COUNT_BITS) - 1; // 约 5 亿
// 线程池 5 种状态(高 3 位)
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..., 任务全空,线程为0,过渡态
private static final int TERMINATED = 3 << COUNT_BITS; // 011..., terminated() 执行完
private final BlockingQueue<Runnable> workQueue; // 任务队列
private final HashSet<Worker> workers = new HashSet<>(); // 工作线程集合
private int largestPoolSize; // 历史峰值线程数
private long completedTaskCount;// 已完成任务总数
}
一个原子变量 ctl 同时维护状态和数量的设计非常精妙,所有状态判断和数量修改都是原子的 CAS 操作。
线程池状态转换图
┌──────────┐
│ RUNNING │
└────┬─────┘
│ shutdown()
┌────▼─────┐
│ SHUTDOWN │ ─────────┐
└────┬─────┘ │ 队列为空
│ shutdownNow() │ 线程为 0
┌────▼─────┐ │
│ STOP │ ─────────┤
└────┬─────┘ │ 线程为 0
│ │
┌────▼─────┐ │
│ TIDYING │ ◄────────┘
└────┬─────┘
│ terminated() 执行完
┌────▼────────┐
│ TERMINATED │
└─────────────┘
Worker 类 — 工作线程的封装
private final class Worker extends AbstractQueuedSynchronizer
implements Runnable {
final Thread thread; // 封装的工作线程
Runnable firstTask; // 构造时指定的首个任务,可为 null
volatile long completedTasks; // 该 Worker 完成的任务计数
Worker(Runnable firstTask) {
setState(-1); // 初始 AQS state=-1,禁止中断直到 runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this); // 核心!循环从队列取任务执行
}
// Worker 的 AQS 实现了一个不可重入的排他锁,
// locked 表示该 Worker 正在执行任务
// unlocked 表示该 Worker 是空闲的
}
runWorker — 核心工作循环
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // 允许中断
boolean completedAbruptly = true;
try {
// ⭐ 核心循环:如果 firstTask 不为空则执行,否则从队列取
while (task != null || (task = getTask()) != null) {
w.lock(); // 执行任务前加锁
// 检查线程池状态,如果已 STOP 则中断
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() && runStateAtLeast(ctl.get(), STOP)))
&& !wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task); // 钩子方法
Throwable thrown = null;
try {
task.run(); // 执行任务
} catch (Throwable x) {
thrown = x; throw x;
} finally {
afterExecute(task, thrown); // 钩子方法
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly); // Worker 退出处理
}
}
getTask — 从队列获取任务
这是理解核心线程如何保持存活的关键:
private Runnable getTask() {
boolean timedOut = false;
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 检查:如果 STOP,或者 SHUTDOWN 且队列为空,减少 Worker 并返回 null
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// ⭐ 决定是否超时取任务
// allowCoreThreadTimeOut=true 或 当前线程数 > corePoolSize → 超时取
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 如果应退出(超时拿到 null 或 线程太多或超时机制触发)
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null; // 返回 null → runWorker 循环退出 → Worker 销毁
continue;
}
try {
// ⭐ 关键区别:
// timed=true → poll(keepAliveTime) — 超时返回 null,Worker 可退出
// timed=false → take() — 永久阻塞等待,Worker 常驻
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null) return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
核心线程如何保持存活?
- 当前线程数 ≤
corePoolSize时,timed = false,调用workQueue.take()永久阻塞等待 - 当线程数 >
corePoolSize时,timed = true,调用workQueue.poll(keepAliveTime)超时等待 - 如果希望核心线程也能回收,设置
allowCoreThreadTimeOut = true
4.2 线程池任务提交流程(完整版)
// execute() 源码分析
public void execute(Runnable command) {
if (command == null) throw new NullPointerException();
int c = ctl.get();
// 步骤 1:当前线程数 < 核心线程数?→ 直接创建新 Worker(addWorker 第二个参数=true)
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true)) // core=true, 计入核心线程
return;
c = ctl.get(); // 创建失败(状态变化),重新获取
}
// 步骤 2:线程池 RUNNING 且入队成功
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 双重检查:入队后线程池可能已经 shutdown,需要回滚
if (!isRunning(recheck) && remove(command))
reject(command); // 回滚后执行拒绝策略
// 或者线程池还在运行,但所有线程都死了(极端情况),创建一个新 Worker
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 步骤 3:队列满了,尝试扩到最大线程数
else if (!addWorker(command, false)) // core=false, 计入非核心线程
reject(command); // 达到最大线程数也放不下,拒绝
}
提交任务
│
▼
┌──────────────┐
│ 核心线程满了? │
└──────┬───────┘
N │ Y
▼ │ ▼
┌───────┐│┌─────────────┐
│addWorker││ workQueue │
│(core=true││ .offer() │
│) ││ 入队成功? │
└───────┘│└──────┬──────┘
│ Y │ N
│ ▼ │ ▼
│ ┌────┐│┌──────────┐
│ │双重 │││addWorker │
│ │检查 │││(core=false│
│ └────┘││) 成功? │
│ │└────┬─────┘
│ │ Y │ N
│ │ ▼ │ ▼
│ │┌───┐│┌──────┐
│ ││执行│││拒绝 │
│ │└───┘││策略 │
│ │ │└──────┘
└───────┴─────┘
4.3 线程池大小配置
如何合理设置核心参数?
int cpuCores = Runtime.getRuntime().availableProcessors();
// ═══ CPU 密集型(计算密集型)═══
// 大量计算、很少阻塞
// 线程数 = CPU 核数 + 1(+1 是为了偶尔的缺页中断/其他暂停时有多余线程顶上去)
int cpuThreads = cpuCores + 1;
// ═══ IO 密集型 ═══
// 大量网络/磁盘 IO,大部分时间在等待
// 根据理论公式:线程数 = CPU核数 × (1 + 平均等待时间/平均计算时间)
// 等待时间/计算时间 的比例很难精确获得,通常取 2 或根据压测调整
// 常见两种公式:
// 公式1:线程数 = CPU核数 × 2
// 公式2:线程数 = CPU核数 / (1 - 阻塞系数)
// 阻塞系数通常取 0.8 ~ 0.9(IO 密集下大部分时间在等 IO)
int ioThreads = cpuCores * 2;
// ═══ 混合型 ═══
// 核心线程数 = (任务总耗时 / CPU计算时间) × CPU核数
// 或者拆分为两个线程池分开执行
最佳实践:公式只是起点,实际值应在压测中确定。
Runtime.getRuntime().availableProcessors() 获取的值:
在 Docker/容器环境中,可能返回宿主机 CPU 核数而非容器限制的核数!
JDK 8u191+ 修复,或用 -XX:ActiveProcessorCount=N 指定
线程池参数动态化(美团方案)
// 核心思路:通过配置中心动态调整参数,无需重启
public class DynamicThreadPool extends ThreadPoolExecutor {
public void setCorePoolSize(int corePoolSize) {
super.setCorePoolSize(corePoolSize);
}
public void setMaximumPoolSize(int maximumPoolSize) {
super.setMaximumPoolSize(maximumPoolSize);
}
// 结合配置中心(Nacos/Apollo),监听参数变化实时调整
}
4.4 拒绝策略详解
| 策略 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy(默认) | 抛出 RejectedExecutionException | 需要感知拒绝、由上层处理 |
| CallerRunsPolicy | 由提交任务的主线程执行 | 不能丢任务的场景,有"反压"效果 |
| DiscardPolicy | 直接丢弃,无任何通知 | 允许丢、不重要(埋点日志、监控采样) |
| DiscardOldestPolicy | 丢弃队首(最旧)任务,重新提交 | 更关注最新数据(实时数据展示) |
CallerRunsPolicy 的"反压"机制:
当队列满时,由调用者线程直接执行任务
→ 调用者线程被占用,无法继续提交新任务
→ 相当于把压力反向传导给上游
→ 形成自然的流量控制(TCP 滑动窗口同理)
自定义拒绝策略:
public class MyRejectedHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 1. 记录日志(包括任务信息、线程池状态)
log.warn("Task rejected: {}, pool size: {}, queue size: {}",
r.toString(), executor.getPoolSize(), executor.getQueue().size());
// 2. 降级处理:写入持久化存储(DB/Redis),后续补偿
// 3. 或者重新尝试提交
if (!executor.isShutdown()) {
try {
executor.getQueue().put(r); // 阻塞式入队
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
execute() vs submit() 异常处理差异(⭐ 重要坑点)
ThreadPoolExecutor pool = new ThreadPoolExecutor(...);
// ❌ 方法1:execute(Runnable) — 任务内未捕获异常 → 直接抛出
pool.execute(() -> {
throw new RuntimeException("将会传播给线程的 UncaughtExceptionHandler");
});
// 线程的 UncaughtExceptionHandler 会收到异常,线程终止,线程池会创建新线程替补
// ⚠️ 方法2:submit(Runnable) — 异常被吞掉!
Future<?> future = pool.submit(() -> {
throw new RuntimeException("静默吞掉,你必须调用 get() 才能拿到");
});
// future.get() 才会抛出 ExecutionException,包装了原始异常
// ✅ 正确做法:submit 后必须 get() 或 自行 try-catch
Future<String> future2 = pool.submit(() -> {
return "result";
});
try {
String result = future2.get(); // get() 才会将任务内的异常通过 ExecutionException 抛出
} catch (ExecutionException e) {
Throwable cause = e.getCause(); // 原始异常
log.error("Task failed", cause);
}
| 维度 | execute(Runnable) | submit(Runnable/Callable) |
|---|---|---|
| 异常传播 | 直接抛给 UncaughtExceptionHandler | 被吞掉,只有 get() 才通过 ExecutionException 抛出 |
| 线程行为 | 异常线程终止,线程池创建新线程替补 | 线程正常返回线程池复用,异常被封装在 Future 中 |
| 返回值 | 无 | 通过 Future.get() 获取返回值 |
| 建议 | 自主 try-catch | 必须 get() 或 try-catch |
最佳实践:
// 线程池统一异常处理方式 1:自定义 ThreadFactory 设置 UncaughtExceptionHandler
ThreadFactory factory = r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler((thread, ex) ->
log.error("Thread {} crashed: {}", thread.getName(), ex.getMessage(), ex)
);
return t;
};
// 线程池统一异常处理方式 2:重写 afterExecute 钩子
ThreadPoolExecutor pool = new ThreadPoolExecutor(...) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (t == null && r instanceof Future<?>) {
try {
((Future<?>) r).get(); // submit 的任务从 Future 拿出异常
} catch (ExecutionException e) {
t = e.getCause();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
if (t != null) {
log.error("Task exception: {}", t.getMessage(), t);
}
}
};
4.5 Executors 四大线程池的风险
// ❌ FixedThreadPool — 无界队列导致 OOM
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()); // ⚠️ 无界!无限堆积
}
// ❌ SingleThreadExecutor — 无界队列导致 OOM
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>())); // ⚠️ 无界!
}
// ❌ CachedThreadPool — 不限线程数导致 OOM
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, // ⚠️ 最大线程数为 Integer.MAX_VALUE
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
// ❌ ScheduledThreadPool — 最大线程数为 Integer.MAX_VALUE
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
// maximumPoolSize = Integer.MAX_VALUE // ⚠️
}
正确创建方式(阿里巴巴规约):
// 生产环境标准写法
ThreadPoolExecutor pool = new ThreadPoolExecutor(
10, // 核心线程数(根据业务配置)
20, // 最大线程数
60L, TimeUnit.SECONDS, // 非核心线程空闲存活时间
new LinkedBlockingQueue<>(1000), // ⚠️ 必须设置有界队列
new ThreadFactoryBuilder() // Guava 工具类
.setNameFormat("biz-pool-%d") // ⚠️ 命名线程,方便排查
.setDaemon(false)
.build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 指定拒绝策略
);
// 使用完记得 shutdown
Runtime.getRuntime().addShutdownHook(new Thread(pool::shutdown));
4.6 线程池监控
// 定期打印线程池核心指标
public class ThreadPoolMonitor {
public static void logPoolStatus(ThreadPoolExecutor pool) {
log.info("ThreadPool [{}]: " +
"active={}, " + // 正在执行任务的线程数
"pool={}, " + // 当前线程数(含空闲)
"core={}, " + // 核心线程数
"max={}, " + // 最大线程数
"largest={}, " + // 历史峰值线程数
"queue={}, " + // 当前队列任务数
"completed={}, " + // 已完成任务总数
"task={} ", // 总任务数 ≈ completed + active + queue
poolName,
pool.getActiveCount(),
pool.getPoolSize(),
pool.getCorePoolSize(),
pool.getMaximumPoolSize(),
pool.getLargestPoolSize(),
pool.getQueue().size(),
pool.getCompletedTaskCount(),
pool.getTaskCount());
}
}
五、JUC 工具类详解
5.1 CountDownLatch / CyclicBarrier / Semaphore
三者对比
| 维度 | CountDownLatch | CyclicBarrier | Semaphore |
|---|---|---|---|
| 核心作用 | 一个线程等待一组操作完成 | 一组线程相互等待到齐 | 控制并发访问数量 |
| 可复用 | ❌ 减到 0 不可重置 | ✅ barrier 可循环使用 | ✅ acquire/release |
| 计数方向 | 递减(countDown -1) | 递增(等待数 +1 到 parties) | 递减/递增(许可的消耗与释放) |
| 等待线程 | 调用方 await 等待 | 所有参与者 await 等待 | 调用方 acquire 等待许可 |
| 底层实现 | AQS 共享模式,state = count | ReentrantLock + Condition | AQS 共享模式,state = permits |
CountDownLatch 源码与原理
// 基于 AQS,state 初始化 = count
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
// countDown: 通过 CAS 将 state - 1,减到 0 时释放所有等待线程
public void countDown() {
sync.releaseShared(1); // tryReleaseShared 在 state == 0 时返回 true
}
// await: 如果 state != 0,则阻塞等待
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1); // tryAcquireShared 在 state == 0 时返回 1
}
使用场景:
- 主线程等待所有子线程初始化完成
- 并行任务全部完成后汇总结果
- 微服务启动时的依赖等待(等所有依赖服务就绪)
CyclicBarrier 源码与原理
// 与 CountDownLatch 不同,CyclicBarrier 内部用 ReentrantLock + Condition 实现
public CyclicBarrier(int parties, Runnable barrierAction) {
// parties: 需要等待的线程数
// barrierAction: 所有线程到齐后执行的回调(最后一个到达的线程执行)
}
// await 内部逻辑(简化):
// 1. 获取 ReentrantLock
// 2. count--(当前代剩余等待数)
// 3. 如果 count == 0:
// a. 执行 barrierAction.run()
// b. nextGeneration() → 唤醒所有等待线程 → count 重置为 parties
// 4. 如果 count > 0:
// 进入 Condition 等待,直到被唤醒或超时/中断
// 如果超时/中断 → breakBarrier() → 唤醒所有线程 → 抛出异常
与 CountDownLatch 的关键区别:
// CountDownLatch: 外部线程等待多个子任务完成
主线程.await() ← 子线程1.countDown()
← 子线程2.countDown()
← 子线程3.countDown()
// CyclicBarrier: 多个线程互相等待,同进同退
子线程1.await() ─┐
子线程2.await() ─┼→ 全部到达 → barrier 打开 → 一起继续执行
子线程3.await() ─┘ → count 重置,可复用
Semaphore 源码与原理
// 基于 AQS,state = 许可证数量
Semaphore semaphore = new Semaphore(3); // 3 个许可证,非公平
Semaphore fairSem = new Semaphore(3, true); // 公平模式
// acquire: 获取 1 个许可证,state-1。如果 state=0,等待直到有许可证
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// release: 释放 1 个许可证,state+1。可能唤醒等待线程
public void release() {
sync.releaseShared(1);
}
典型场景:
- 数据库连接池限流(最多 N 个线程同时访问数据库)
- 接口限流(并发度控制)— 实现漏桶/令牌桶的基础
- 停车位管理(N 个车位,来一个车占一个,走一个车释放一个)
5.2 ConcurrentHashMap 深度解析
JDK7 → JDK8 架构演进
JDK 7: Segment 分段锁
┌─────────────────────────────────────────────┐
│ ConcurrentHashMap │
│ ┌──────────┬──────────┬──────────┬───────┐ │
│ │Segment[0]│Segment[1]│Segment[2]│ ... │ │ 每个 Segment 是一把 ReentrantLock
│ │ ┌────┐ │ ┌────┐ │ ┌────┐ │ │ │ 默认 16 个 Segment,并发度 16
│ │ │Hash│ │ │Hash│ │ │Hash│ │ │ │
│ │ │Table│ │ │Table│ │ │Table│ │ │ │ 要定位两次:先定位 Segment,再定位 HashEntry
│ │ └────┘ │ └────┘ │ └────┘ │ │ │
│ └──────────┴──────────┴──────────┴───────┘ │
└─────────────────────────────────────────────┘
JDK 8: CAS + synchronized 细粒度锁
┌─────────────────────────────────────────────┐
│ ConcurrentHashMap │
│ ┌─────┬─────┬─────┬─────┬─────┬─────┬───┐ │
│ │ [0] │ [1] │ [2] │ [3] │ [4] │ [5] │...│ │ Node<K,V> 数组
│ │ │ ● │ │ │ │ │ │ │ 每个桶:
│ └─────┴──┼──┴─────┴─────┴─────┴─────┴───┘ │ 空桶 → CAS 直接 put
│ │ synchronized │ 有值 → synchronized 锁桶头
│ ┌─────▼────┐ │
│ │ Node │ → Node → ... (链表) │ 链表 > 8 → 红黑树
│ └──────────┘ │
└─────────────────────────────────────────────┘
JDK8 ConcurrentHashMap 关键字段
public class ConcurrentHashMap<K,V> {
transient volatile Node<K,V>[] table; // 当前数组,volatile 保证可见性
private transient volatile Node<K,V>[] nextTable; // 扩容时的目标数组
private transient volatile int sizeCtl; // 多义性字段
// sizeCtl 含义:
// -1: 正在初始化
// -(1+N): 有 N 个线程正在协助扩容
// >0: 初始化前的容量 或 扩容阈值 (0.75n)
static class Node<K,V> {
final int hash;
final K key;
volatile V val; // volatile 保证可见性
volatile Node<K,V> next; // volatile 保证可见性
}
}
put 方法完整流程
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException(); // ⚠️ 不支持 null
int hash = spread(key.hashCode()); // 扰动函数,降低冲突
int binCount = 0;
for (Node<K,V>[] tab = table;;) { // 自旋循环
Node<K,V> f; int n, i, fh;
// ① 如果 table 为空或长度为 0 → 初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// ② 桶为空 → CAS 直接放入,无锁
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // CAS 成功,直接返回
}
// ③ 遇到 ForwardingNode (hash == MOVED(-1)) → 帮助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// ④ 桶不为空 → synchronized 锁住桶头节点
else {
V oldVal = null;
synchronized (f) { // ⭐ 只锁当前桶的头节点
if (tabAt(tab, i) == f) { // 双重检查
if (fh >= 0) { // 普通链表节点
binCount = 1;
// 遍历链表,找到相同的 key 则更新,否则追加到尾部
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key || (ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent) e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
}
else if (f instanceof TreeBin) { // 红黑树节点
// ... 树操作 ...
}
}
}
if (binCount != 0) {
// 链表长度 >= 8 → 转为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
// ...
break;
}
}
}
// ⑤ 计数 + 1,并检查是否需要扩容
addCount(1L, binCount);
return null;
}
关键设计点:
- 空桶 CAS:没有锁竞争,高效
- synchronized 只锁桶头:锁粒度最细
- 遇到 ForwardingNode 帮助扩容:多线程协同迁移
- initTable 也用 CAS:保证只有一个线程初始化
get 方法 — 完全无锁
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 桶头恰好命中
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// hash < 0: 可能是树节点或 ForwardingNode
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 普通链表遍历
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
// 全程无锁,靠 volatile 保证 Node.val 和 Node.next 的可见性
扩容机制 — 多线程协同迁移
// 扩容的两个核心方法
// 1. transfer:将原 table 的数据迁移到新 table
// 2. helpTransfer:发现正在扩容时,当前线程帮忙迁移
// 扩容过程(简化为 5 步):
// ① 一个线程触发扩容(put 后 addCount 发现超过阈值)
// ② 创建新的 2 倍大小的数组
// ③ 将原数组按步长(如每个线程负责16个桶)分段
// ④ 每个参与线程处理自己负责的桶区间:
// - 链表:构造高位链和低位链(根据 hash & n 是否为 0 分流)
// - 树:类似处理
// ⑤ 迁移完成后,在老数组桶位放置 ForwardingNode 标记"已迁移"
// ForwardingNode: hash = MOVED(-1),作用有3个:
// 1. 标记该桶已经迁移完成
// 2. find() 方法转发到新数组查找
// 3. 其他线程读到 ForwardingNode 知道正在扩容,参与协助
size 计数 — 分段计数避免竞争
// JDK8 不使用 JDK7 的 segment.modCount 累加方式
// 而是使用类似 LongAdder 的分段计数:
// 无竞争时直接 CAS baseCount
// 有竞争时使用 CounterCell[] 分散热点,最终 sum = baseCount + ΣCounterCell[].value
// addCount 简化逻辑:
// 1. 先 CAS baseCount+1,成功则返回
// 2. 失败 → 线程生成随机数,落到 CounterCell[x]
// 3. CAS CounterCell[x].value+1
// 4. 检查是否需要扩容
// size() / mappingCount() 求和:
// long sum = baseCount
// for (CounterCell cell : counterCells)
// sum += cell.value
// 注意:size() 返回 int 可能溢出,推荐用 mappingCount() 返回 long
JDK7 vs JDK8 差异总结
| 对比维度 | JDK 7 | JDK 8 |
|---|---|---|
| 数据结构 | Segment 数组 + HashEntry 链表 | Node 数组 + 链表/红黑树 |
| 锁机制 | Segment(继承 ReentrantLock) | CAS + synchronized(桶头) |
| 锁粒度 | Segment 级别(默认 16 段) | 桶级别(最细) |
| get 操作 | 需要加锁(volatile 读) | 完全无锁 + volatile 保证可见性 |
| 并发度 | 固定 16(构造函数指定,不可改) | 桶数量级(table 长度,可动态扩容) |
| 扩容 | 单线程迁移 Segment 内部 | 多线程协同迁移(transfer) |
| 链表→红黑树 | ❌ 全是链表 | ✅ 链表 > 8 且 table >= 64 时转红黑树 |
| size 计算 | 先不加锁算 3 次,不一致再加锁 | 分段累加(baseCount + CounterCell[]) |
| null 键值 | ❌ 不支持 | ❌ 不支持 |
HashMap 并发问题详解(⭐ 重要背景)
HashMap 为什么线程不安全?
// JDK7:并发 resize 导致链表死循环(环形链表)
// 头插法:resize 扩容时将旧链表元素迁移到新数组,使用头插法
// 线程 A resize 时形成反向链表,线程 B 继续遍历 → 环形链表 → CPU 100% 死循环
// JDK8:不再死循环(改用尾插法),但仍有数据丢失问题
// 两个线程同时 put 时,一个线程的数据可能被另一个覆盖
// counter 累加非原子操作,导致 size 不准确
对比三种 Map:
| 维度 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | ❌ 不安全 | ✅ synchronized 全表锁 | ✅ CAS + synchronized 桶锁 |
| 性能 | 高 | 低(全表锁,串行化) | 高(细粒度锁) |
| null 键值 | ✅ 允许 1 个 null key | ❌ 不允许 | ❌ 不允许(NullPointerException) |
| JDK8 结构 | 数组 + 链表/红黑树 | 数组 + 链表 | 数组 + 链表/红黑树 + 多线程协同扩容 |
| 迭代器 | fail-fast | Enumeration/Iterator | 弱一致性(不抛 CME) |
| 推荐度 | 单线程 | ❌ 已废弃 | ⭐⭐⭐⭐⭐ |
Hashtable 已废弃,所有方法加
synchronized,同一时刻只能一个线程操作,高并发下退化为串行。
⚠️ CHM 复合操作仍需额外同步
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// ❌ 错误:单个 put/get 是线程安全的,但复合操作不是原子的!
Integer count = map.get("key");
if (count == null) {
map.put("key", 1); // 两个线程可能同时到达这里
} else {
map.put("key", count + 1); // 读-改-写三步,不是原子的
}
// ✅ 正确方式1:使用 CHM 提供的原子复合操作
map.putIfAbsent("key", 1); // 不存在才 put
map.compute("key", (k, v) -> v == null ? 1 : v + 1); // 原子计算
map.computeIfAbsent("key", k -> 1); // 不存在才计算
map.merge("key", 1, Integer::sum); // 原子合并
// ✅ 正确方式2:外置加锁
synchronized (map) {
Integer c = map.get("key");
map.put("key", c == null ? 1 : c + 1);
}
CHM 原子复合操作速查:
| 方法 | 含义 |
|---|---|
putIfAbsent(K, V) | Key 不存在时才插入 |
remove(K, V) | Key 和 Value 都匹配时才删除 |
replace(K, V1, V2) | 旧值匹配才替换 |
compute(K, BiFunction) | 原子计算(整个过程桶级加锁) |
computeIfAbsent/IfPresent | 按条件计算 |
merge(K, V, BiFunction) | 原子合并 |
5.3 CopyOnWriteArrayList
核心思想:读操作完全无锁,写操作复制新数组后在副本上修改,然后原子替换引用。
public class CopyOnWriteArrayList<E> {
// ⭐ volatile 数组引用,保证写操作的可见性
private transient volatile Object[] array;
// 读操作:直接读原数组,无锁
public E get(int index) {
return elementAt(array, index);
}
// 写操作:加 ReentrantLock,复制新数组,修改后原子替换
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1); // 复制新数组(内存拷贝)
es[len] = e;
setArray(es); // 原子替换引用(volatile 写)
return true;
}
}
// 迭代器:快照(Snapshot)思想
// 迭代器创建时保存了当时 array 的引用,后续的写操作都在新数组上进行
// 所以迭代器完全不受并发写影响,也不抛 ConcurrentModificationException
}
优缺点对比:
| 优点 | 缺点 |
|---|---|
| 读操作完全无锁,高并发读性能极好 | 写操作复制整个数组,内存开销 O(n) |
| 读写分离,写不影响读 | 数据弱一致性(读可能读到旧数据) |
| 迭代器是快照,不会抛 CME | GC 压力大(旧数组变垃圾) |
适用场景:读多写极少 — 黑名单、白名单、监听器列表、配置信息
不适用场景:写操作频繁(每次写都复制整个数组)、需要强一致性的场景
5.4 BlockingQueue 详解
| 实现类 | 底层结构 | 有界/无界 | 锁机制 | 特点 |
|---|---|---|---|---|
| ArrayBlockingQueue | 数组(环形队列) | 必须有界 | 1 把锁 put+take 共用 | 结构简单,内存预分配,公平锁可选 |
| LinkedBlockingQueue | 单向链表 | 可选有界(默认 Integer.MAX_VALUE) | 2 把锁 putLock + takeLock | 读写分离,吞吐量更高 |
| SynchronousQueue | 无存储空间 | 零容量 | — | 生产者直接传递给消费者,一对一交接 |
| PriorityBlockingQueue | 数组实现的堆 | 无界 | 1 把锁 | 按优先级出队,无界需注意 OOM |
| DelayQueue | PriorityQueue | 无界 | 1 把锁 | 延迟到期才能出队(ScheduledThreadPool 内部使用) |
| LinkedTransferQueue | 单向链表 | 无界 | CAS + 自旋 | JDK7+, 结合了 SynchronousQueue 和 LinkedBlockingQueue |
生产者-消费者模式(经典写法):
// ArrayBlockingQueue 实现
BlockingQueue<Task> queue = new ArrayBlockingQueue<>(100);
// 生产者
queue.put(task); // 阻塞直到有空间
queue.offer(task); // 不阻塞,满了返回 false(推荐业务代码用这个,有限时间)
// 消费者
Task task = queue.take(); // 阻塞直到有元素
Task task = queue.poll(1, TimeUnit.SECONDS); // 超时等待
5.5 CompletableFuture 深度解析
核心 API 分类
// ═══════ 1. 创建 CompletableFuture ═══════
CompletableFuture<Void> cf1 = CompletableFuture.runAsync(() -> {});
CompletableFuture<T> cf2 = CompletableFuture.supplyAsync(() -> result);
// ═══════ 2. 转换(Transform): 输入→处理→新输出 ═══════
cf.thenApply(fn) // T → U,同步
.thenApplyAsync(fn) // T → U,异步
// ═══════ 3. 消费(Consume): 消费结果,无返回 ═══════
cf.thenAccept(consumer) // T → Void
.thenRun(runnable) // 无入参,执行一段逻辑
// ═══════ 4. 组合(Combine): 两个都完成 ═══════
cf1.thenCombine(cf2, bifn) // (T, U) → V
cf1.thenCompose(fn) // T → CompletableFuture<U>,展平
// ═══════ 5. 选择(Either): 任一完成 ═══════
cf1.applyToEither(cf2, fn) // 取先完成的那个
cf1.acceptEither(cf2, consumer)
// ═══════ 6. 多任务 ═══════
CompletableFuture.allOf(cf1, cf2, cf3) // 全部完成
CompletableFuture.anyOf(cf1, cf2, cf3) // 任一完成
// ═══════ 7. 异常处理 ═══════
cf.exceptionally(ex -> fallback) // 异常兜底
.handle((result, ex) -> ...) // 正常/异常都处理
.whenComplete((result, ex) -> ...) // 正常/异常都执行(无返回值,观察者模式)
典型使用场景
// 场景1:异步调用链
CompletableFuture.supplyAsync(() -> rpcService.queryUser(userId))
.thenApplyAsync(user -> rpcService.queryOrders(user))
.thenAcceptAsync(orders -> saveToCache(orders))
.exceptionally(ex -> {
log.error("query failed", ex);
return null;
});
// 场景2:并行调用,合并结果
CompletableFuture<User> userFuture =
CompletableFuture.supplyAsync(() -> userService.getUser(id));
CompletableFuture<Order> orderFuture =
CompletableFuture.supplyAsync(() -> orderService.getOrders(id));
String result = userFuture
.thenCombine(orderFuture, (user, orders) -> user.getName() + orders.size())
.get(3, TimeUnit.SECONDS);
// 场景3:双数据源竞速
CompletableFuture<String> fromDB =
CompletableFuture.supplyAsync(() -> db.query(sql));
CompletableFuture<String> fromRedis =
CompletableFuture.supplyAsync(() -> redis.get(key));
fromDB.applyToEither(fromRedis, result -> result)
.orTimeout(500, TimeUnit.MILLISECONDS) // JDK9+
.exceptionally(ex -> "fallback");
5.6 ForkJoinPool
// ForkJoinPool 是 JDK7 引入的分治任务执行框架
// CompletableFuture.supplyAsync() 默认用的就是 ForkJoinPool.commonPool()
// 核心思想:工作窃取(Work Stealing)
// 每个工作线程有自己的双端队列
// 线程执行完自己的任务后,从其他繁忙线程的队列尾部"偷"任务来执行
工作窃取示意图:
线程 A (忙): [task1 ← task2 ← task3] ← pop/push 操作这端
线程 B (闲): 自己的队列空了 → 从 A 的队列尾部 steal task1
线程 A: 继续处理 task2, task3,两个线程并行工作
ForkJoinTask 的两个子类:
RecursiveTask<V>:有返回值的任务RecursiveAction:无返回值的任务
// 经典示例:并行计算 1+2+...+N
class SumTask extends RecursiveTask<Long> {
static final int THRESHOLD = 10000;
long[] array; int low; int high;
@Override
protected Long compute() {
if (high - low <= THRESHOLD) {
long sum = 0;
for (int i = low; i < high; i++) sum += array[i];
return sum;
}
int mid = (low + high) >>> 1;
SumTask left = new SumTask(array, low, mid);
SumTask right = new SumTask(array, mid, high);
left.fork(); // 异步执行左半部分
Long rightResult = right.compute(); // 同步执行右半部分
Long leftResult = left.join(); // 等待左半部分
return leftResult + rightResult;
}
}
ForkJoinPool pool = new ForkJoinPool();
long result = pool.invoke(new SumTask(array, 0, array.length));
ForkJoinPool vs ThreadPoolExecutor:
| 对比 | ThreadPoolExecutor | ForkJoinPool |
|---|---|---|
| 任务队列 | 一个共享 BlockingQueue | 每个线程有自己的双端队列 |
| 线程空闲时 | 阻塞等待新任务 | 窃取其他线程的任务 |
| 适合场景 | 独立、无依赖的任务 | 可分解、有依赖的分治任务 |
| CPU 利用 | 可能有线程空闲 | 更高,因为工作窃取 |
六、面试高频问题速答
Q1:synchronized 和 Lock 的区别?什么时候用哪个?
| 维度 | synchronized | Lock(ReentrantLock) |
|---|---|---|
| 实现 | JVM 关键字,monitor | JDK API,AQS |
| 释放 | 自动(代码块退出/异常) | 手动 finally{unlock()} |
| 中断 | ❌ 等锁时不可中断 | ✅ lockInterruptibly() |
| 超时 | ❌ | ✅ tryLock(timeout) |
| 公平 | ❌ 非公平 | ✅ 可选 |
| 条件 | ❌ 单条件 wait/notify | ✅ 多 Condition |
| 性能 | JDK6+ 优化后差距不大 | 高竞争场景略优 |
选择原则:
- 能用 synchronized 就用(代码更安全、更简洁)
- 需要超时、可中断、多条件、tryLock 时 → Lock
Q2:volatile 的底层实现和为什么不能保证原子性?
底层:内存屏障(Memory Barrier)
- volatile 写 → 前插 StoreStore,后插 StoreLoad → 强制刷回主内存
- volatile 读 → 后插 LoadLoad、LoadStore → 强制从主内存读
不能保证原子性:i++ 包含读→改→写三步,volatile 只保证每一步的值是最新的,但无法保证三步之间不被其他线程插入。
Q3:ThreadLocal 内存泄漏原因和解决方案?
原因:ThreadLocalMap 的 Entry 的 Key 是弱引用(ThreadLocal 对象),Value 是强引用。ThreadLocal 被 GC → Key 变 null → Value 无法访问但通过线程引用链可达 → 只要线程不销毁就永久占用。
解决方案:用完必须 remove(),特别是线程池场景。
Q4:线程池提交任务的执行流程?
- < 核心线程数 → 创建核心线程
- 核心线程满 → 入阻塞队列
- 队列满 → 创建线程直到达到最大线程数
- 达到最大且队列满 → 拒绝策略
Q5:ConcurrentHashMap 如何保证线程安全?
JDK7:Segment + ReentrantLock,默认 16 段,每段独立加锁。
JDK8:CAS + synchronized + volatile。
- put:空桶 CAS 直插,有冲突 synchronized 锁桶头
- get:完全无锁,volatile 保证可见性
- 扩容:多线程协同迁移(helpTransfer)
- size:分段累加(baseCount + CounterCell[])
Q6:如何排查死锁?
jps -l # 找 PID
jstack <pid> # 末尾自动检测死锁,打印死锁信息
# 或 Arthas: thread -b # 一键找阻塞源头
Q7:wait() 和 sleep() 的区别?
| wait() | sleep() | |
|---|---|---|
| 所属类 | Object | Thread |
| 锁释放 | ✅ 释放锁 | ❌ 不释放锁 |
| 调用条件 | 必须在 synchronized 中 | 任意位置 |
| 唤醒方式 | notify()/notifyAll() | 超时或 interrupt() |
| 状态 | WAITING | TIMED_WAITING |
Q8:什么是伪共享(False Sharing)?如何解决?
CPU 缓存以缓存行(Cache Line,通常 64 字节)为单位,多个线程修改不同变量但变量在同一缓存行时,导致缓存行反复失效。
CPU1 更新 X → 使 CPU2 的缓存行失效
CPU2 更新 Y → 使 CPU1 的缓存行失效
即使 X 和 Y 没有任何关系,性能也大幅下降
解决方案:
- JDK8
@Contended注解(需-XX:-RestrictContended) - 手动填充(padding)把变量撑开,占满一个缓存行
Q9:为什么 wait()/notify() 要定义在 Object 中,而不是 Thread 中?
每个对象都可以作为锁,锁的作用范围是任意对象。如果定义在 Thread 中,那每个线程只能有一个等待队列,无法区分不同锁的等待条件。放在 Object 中,每个对象都可以有自己的等待队列。
Q10:notify() 和 notifyAll() 的区别?
notify():随机唤醒一个在 WaitSet 中等待的线程notifyAll():唤醒所有等待线程,但它们还要重新竞争锁
建议:始终使用 notifyAll(),除非你非常确定只有一个线程在等待且永远不会增加。因为只用 notify() 可能导致信号丢失(唤醒了错误的线程)。
Q11:线程池中,核心线程会被回收吗?
默认不会。核心线程调用 workQueue.take() 永久阻塞等待。
设置 pool.allowCoreThreadTimeOut(true) 后,核心线程在空闲超过 keepAliveTime 后也会被回收。
Q12:synchronized 锁升级过程?
无锁 → 偏向锁 → 轻量级锁 → 重量级锁。只能升级,不能降级(偏向锁可撤销为无锁)。
- 偏向锁:同一线程反复获取,Mark Word 存线程 ID
- 轻量级锁:交替获取无竞争,CAS 自旋
- 重量级锁:并发竞争,OS Mutex,线程阻塞
Q13:HashMap 和 ConcurrentHashMap 的区别?
| 维度 | HashMap | ConcurrentHashMap |
|---|---|---|
| 线程安全 | ❌ 线程不安全 | ✅ 线程安全 |
| null 键值 | ✅ 允许 null | ❌ 不允许 null |
| 并发控制 | 无 | CAS + synchronized |
| 迭代器 | fail-fast | 弱一致性(不抛 CME) |
| JDK8 结构 | 数组+链表+红黑树 | 同,且扩容多线程协同 |
七、并发工具速查表
| 工具 | 核心机制 | 一句话用途 |
|---|---|---|
AtomicInteger | CAS 自旋 | 无锁原子整数 |
LongAdder | 分段累加 Cell[] | 高并发累加(统计 QPS) |
AtomicStampedReference | CAS + 版本号 | 解决 ABA 问题 |
CountDownLatch | AQS 共享模式 | 等人齐/等任务完 |
CyclicBarrier | Lock + Condition | 大家都到齐,一起出发 |
Semaphore | AQS 共享模式 | 限流、控制并发数 |
Exchanger | CAS + park/unpark | 两个线程交换数据 |
Phaser | 多阶段同步 | 增强版 CyclicBarrier + CountDownLatch |
CompletableFuture | 异步任务编排 | 回调地狱的终结者 |
ForkJoinPool | 工作窃取 | 分治任务并行计算 |
ConcurrentLinkedQueue | CAS + 自旋 | 无界并发队列 |
LinkedBlockingQueue | 双锁 putLock + takeLock | 生产者-消费者队列 |
SynchronousQueue | 无缓冲直接传递 | 线程一对一直接交接 |
DelayQueue | PriorityQueue + 时间判定 | 定时任务队列 |
八、JVM 层面对并发的支持
8.1 锁优化
| 优化 | 触发条件 | 原理 |
|---|---|---|
| 偏向锁 | 单线程反复获取 | 对象头记录线程 ID,无 CAS |
| 轻量级锁 | 交替获取无竞争 | 栈中 Lock Record + CAS |
| 重量级锁 | 并发竞争 | OS Mutex |
| 自旋锁 | 锁持有时间短 | 忙等不挂起 |
| 自适应自旋 | JMV 统计 | 根据前次成功率动态调整 |
| 锁消除 | 逃逸分析无逃逸 | 直接删除 synchronized |
| 锁粗化 | 连续加解锁 | 合并为一次 |
8.2 指令重排序与内存屏障
源代码
│ ① 编译器重排序
▼
编译后指令
│ ② 指令级并行重排序
▼
CPU 执行序列
│ ③ 内存系统重排序
▼
最终对其他处理器可见的内存访问顺序
8.3 逃逸分析(Escape Analysis)
JIT 的分析手段,判断对象是否逃逸出方法/线程:
- 无逃逸 → 栈上分配 → 无需 GC,方法结束自动回收
- 无逃逸 → 锁消除 → 去掉不必要的同步
- 无逃逸且不改变字段 → 标量替换 → 拆成基本类型直接放栈上
九、坑点小结(⭐ 面试/生产必知)
| 序号 | 坑点 | 一句话 | 正确做法 |
|---|---|---|---|
| 1 | volatile 不保证原子性 | volatile int i++; 线程不安全 | 用 AtomicInteger / synchronized |
| 2 | wait 必须在 synchronized 内 | 否则抛 IllegalMonitorStateException | 先拿到锁,再 wait() |
| 3 | 线程池 submit 吞异常 | 不 get() 永远不知道任务异常 | get() 或重写 afterExecute |
| 4 | ThreadLocal 必须 remove | 线程池复用导致数据污染和内存泄漏 | finally { tl.remove(); } |
| 5 | CHM 复合操作非原子 | get + put 两步,中间可被插入 | 用 compute/merge/外部加锁 |
| 6 | Executors 创建线程池 | 无界队列或无限线程 → OOM | 手动 new ThreadPoolExecutor |
| 7 | DCL 单例不加 volatile | 指令重排导致半初始化对象 | private static volatile Singleton instance; |
| 8 | 锁释放顺序要对称 | 外层锁先获取后释放 | 加锁顺序 = 解锁逆序(或一致也可以,但要统一) |
| 9 | notify 而非 notifyAll | 可能唤醒错误的等待线程,信号丢失 | 默认用 notifyAll() |
| 10 | HashMap 并发 put | JDK7 死循环,JDK8 数据丢失 | 用 ConcurrentHashMap |
| 11 | interrupt 不清除中断标志 | isInterrupted() 检查后标志仍在 | interrupted() 检查并清除(静态方法) |
| 12 | 主线程不感知子线程异常 | 子线程未捕获异常,主线程继续走 | Future.get() / UncaughtExceptionHandler |
📌 记忆口诀:
- 线程池用 ThreadPoolExecutor 手动创建,七大参数要记牢
- volatile 可见有序不原子,DCL 单例必须加
- synchronized 自动释放会升级,Lock 手动释放高级多
- CAS 无锁自旋看 ABA,LongAdder 分段高并发
- CHM JDK7 分段变 JDK8 CAS+锁,get 无锁扩容多线程帮
- ThreadLocal 用完必须 remove,弱引用 Key 强引用 Value 是祸根
- AQS 是并发基石,CLH 队列 + volatile state
- CompletableFuture 编排异步,allOf/anyOf/thenCombine 三板斧
- 坑点记牢:volatile 不原子 / wait 里放锁 / submit 要 get / ThreadLocal 要 remove / Executors 别用 / CHM 复合操作要同步
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)