从对象头到 AQS:Java 锁机制的底层博弈
synchronized 锁升级、ReentrantLock 实现原理、CAS/ABA 全解析
包含:锁升级、AQS、可重入、公平锁、Condition、CAS、ABA、Monitor.
一、线程基础
1.1 run() 和 start() 的区别(重要)
这是 Java 多线程中容易混淆的两个方法。
1.1.1 run() 方法
run() 方法定义了线程要执行的任务代码,但它只是一个普通方法。
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行的任务");
}
}
// 错误用法
MyThread t = new MyThread();
t.run(); // 这不会创建新线程!main线程直接调用run()方法
执行流程:
1.1.2 start() 方法
start() 方法会创建新线程,并由新线程自动执行 run() 方法。
// 正确用法
MyThread t = new MyThread();
t.start(); // 创建新线程,新线程执行run()方法
执行流程:
1.1.3 完整对比示例
public class RunVsStart {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务");
});
// 情况1:调用 run()
t.run(); // 输出:main 执行任务(没有创建新线程)
// 情况2:调用 start()
t.start(); // 输出:Thread-0 执行任务(创建了新线程)
}
}
1.1.4 为什么不能多次调用 start()?
Thread t = new Thread(() -> {});
t.start(); // 第一次调用:正常
t.start(); // 第二次调用:抛出 IllegalThreadStateException
原因:start() 内部会设置线程状态,一个线程只能启动一次,这是 JVM 的设计规定。
1.2 并发与并行(核心概念区别)
1.2.1 并发(Concurrent)
定义:多个任务在同一时间段内启动和执行,但在任意单一时间点,只有一个任务在 CPU 上运行。
实现方式:
- 单核 CPU 通过时间片轮转快速切换任务
- 每个任务执行一小段时间(比如 10ms),然后切换到下一个
- 因为切换速度很快,用户感觉多个任务在"同时"执行
图示:
时间 →
线程A: [—-] [—-] [—-]
线程B: [—-] [—-] [—-]
线程C: [—-] [—-] [—-]
↑ 任意时间点只有1个线程在执行
比喻:一个人同时吃三个馒头。他先咬一口馒头A,再咬一口馒头B,然后咬一口馒头C,循环往复。看起来三个馒头都在被吃,但同一时间只咬一口。
1.2.2 并行(Parallel)
定义:多个任务在同一时间点真正地同时执行。
实现方式:
- 需要多核 CPU(至少 2 个核心)
- 每个核心独立执行一个任务
- 真正的同时执行,没有切换开销
图示:
时间 →
CPU核心1: [========线程A========]
CPU核心2: [========线程B========]
CPU核心3: [========线程C========]
↑ 同一时刻,3个线程都在执行
比喻:三个人,一人吃一个馒头,同时咬、同时嚼、同时咽。
1.2.3 并发 vs 并行:决定性对比
| 硬件要求 | 单核或多核均可 | 必须多核 |
| 执行方式 | 快速切换,宏观同时微观串行 | 真正的同时执行 |
| 目的 | 提高程序响应性(处理多个 I/O) | 提高计算速度(利用多核) |
| 关注点 | 结构设计(如何处理多个任务) | 性能优化(如何更快完成) |
| 难度 | 较低(单核无竞态条件?) | 较高(真正并行需要同步) |
1.2.4 经典组合:并发 + 并行
现代多核系统通常同时使用并发和并行:
- 系统层面:有 100 个线程在"并发"运行
- 硬件层面:8 个 CPU 核心在"并行"执行其中 8 个线程
1.3 什么时候需要多线程?
1.3.1 场景一:处理"慢"操作,防止界面卡死(最重要,占 90%)
这是最普遍的需求。任何可能"卡住"程序的操作,都需要多线程。
典型场景:
- 网络请求(HTTP 调用、下载文件)
- 文件读写(大文件读取、保存)
- 数据库查询(复杂 SQL、大数据量)
- 图片/视频处理(压缩、转码、滤镜)
- 邮件发送(SMTP 可能超时)
问题演示(单线程的灾难):
// 假设计算器应用,没有使用多线程
button.addClickListener(event -> {
// 下载一个 100MB 的文件
downloadFile("http://example.com/bigfile.zip"); // 耗时 30 秒
// 在这 30 秒内,整个应用界面完全"冻住"
// 用户无法点击任何按钮,无法关闭窗口
// 用户会认为程序崩溃了,强行结束进程
});
解决方案(多线程拯救体验):
button.addClickListener(event -> {
// 创建后台线程处理耗时操作
new Thread(() -> {
downloadFile("http://example.com/bigfile.zip");
// 下载完成后,再通过 UI 线程更新界面
}).start();
// 主线程立即返回,界面保持流畅
});
1.3.2 场景二:充分利用多核 CPU(并行计算)
当任务是计算密集型(CPU-bound)时,多线程可以大幅提升速度。
典型场景:
- 大数据集处理(数组排序、聚合计算)
- 科学计算(矩阵乘法、Monte Carlo 模拟)
- 视频/音频渲染和编码
- 机器学习模型训练
- 加密/解密大量数据
量化效果:
// 任务:计算 1 + 2 + 3 + … + 10亿 = ?
// 单线程:耗时 4 秒(一个核工作,其他 7 个核空闲)
// 8 线程并行:耗时 0.5 秒(8 个核同时工作)
// 加速比:8 倍(理论值,实际接近 7.5 倍)
并行化模式:
// 分治模式
long[] numbers = new long[1_000_000_000];
int coreCount = Runtime.getRuntime().availableProcessors(); // 获取核心数
int chunkSize = numbers.length / coreCount;
// 为每个核心分配一个子任务
for (int i = 0; i < coreCount; i++) {
int start = i * chunkSize;
int end = (i + 1) * chunkSize;
threads[i] = new Thread(() -> sumRange(numbers, start, end));
threads[i].start();
}
// 等待所有子任务完成,汇总结果
1.3.3 场景三:提高响应速度,分摊初始化任务
程序启动时需要做多个独立准备工作。
典型场景:
- App/游戏启动:同时加载配置、资源、网络连接
- 服务器启动:同时初始化多个服务
- IDE 启动:同时扫描插件、索引文件、加载界面
对比:
单线程启动:
加载配置 (1秒) → 加载图片 (2秒) → 建立网络 (1秒) → 检查更新 (2秒) = 6秒
多线程启动:
加载配置 (1秒) ────┐
加载图片 (2秒) ────┤
建立网络 (1秒) ────┼─ 同时进行 = 2秒(最慢的任务)
检查更新 (2秒) ────┘
1.3.4 什么时候不需要多线程?
| 简单运算(a + b) | 创建线程的开销(几微秒)比运算本身(纳秒级)还大 |
| 少量数据的内存操作 | 不值得引入线程管理的复杂度 |
| 顺序强依赖的任务 | 任务 B 必须等任务 A 完成后才能开始 |
| 调试阶段 | 多线程 Bug 极难定位,先在单线程验证逻辑 |
1.3.5 决策流程图
开始
│
▼
任务是否需要等待 I/O?
(网络/文件/数据库)
│
├─ 是 ──▶ 必须使用多线程(防止卡死)
│
▼ 否
任务计算量大且可拆分?
(>1秒的 CPU 计算)
│
├─ 是 ──▶ 考虑使用多线程(利用多核)
│
▼ 否
任务包含多个独立子任务?
(启动加载/并行处理)
│
├─ 是 ──▶ 可以用多线程提升体验
│
▼ 否
单线程足够满足需求
│
▼
保持简单,不要过度设计
二、synchronized 深度解析
2.1 为什么需要 synchronized?
多线程的经典问题:竞态条件
public class Counter {
private int count = 0;
public void increment() {
count++; // 这不是原子操作!
}
}
// 1000 个线程各执行 1000 次 increment
// 期望结果:1,000,000
// 实际结果:随机值(如 998,543),小于期望值
为什么 count++ 不是原子的?
// count++ 在字节码层面是 4 步操作
// 1. 从内存读取 count 值到寄存器
// 2. 在寄存器中 +1
// 3. 将新值写回内存
// 4. 内存屏障(可选)
// 两个线程并发执行:
线程A读取 count=100
线程B读取 count=100 ← 同时读到相同值
线程A计算 100+1=101
线程B计算 100+1=101
线程A写回 101
线程B写回 101
// 结果:两次操作,只增加了 1
synchronized 的作用:保证同一时间只有一个线程执行代码块,从而解决竞态条件。
2.2 对象头与 Monitor(JVM 级别的重要实现)
2.2.1 Java 对象的内存布局
每个 Java 对象在内存中由三部分组成:
┌──────────────────────────────────────┐
│ 对象头 (Header) │ ← 16 字节(32位JVM)或 12 字节(64位)
│ ┌────────────────────────────────┐ │
│ │ Mark Word (标记字段) │ │ ← 存储 hashCode、GC 信息、锁信息
│ ├────────────────────────────────┤ │
│ │ Klass Pointer (类型指针) │ │ ← 指向类的元数据
│ └────────────────────────────────┘ │
├──────────────────────────────────────┤
│ 实例数据 (Instance Data) │ ← 成员变量
├──────────────────────────────────────┤
│ 对齐填充 (Padding) │ ← 8 字节对齐
└──────────────────────────────────────┘
2.2.2 Mark Word 的锁状态编码
Mark Word 中存储了锁信息,根据锁状态不同,解释方式也不同:
| 无锁 | hashCode(25)+ age(4) + 0/1 | 01 |
| 偏向锁 | 线程ID(23)+ epoch(2)+ age(4) | 01 |
| 轻量级锁 | 指向栈中 Lock Record 的指针 | 00 |
| 重量级锁 | 指向 Monitor 对象的指针 | 10 |
| GC 标记 | 空 | 11 |
2.2.3 Monitor 结构详解(非常重要)
Monitor 是 synchronized 的底层实现机制,每个对象都可以关联一个 Monitor(通过对象头的指针)。
Monitor 完整结构
┌─────────────────────────────────────────────────────────────┐
│ Monitor │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Owner(拥有者) │ │
│ │ 指向当前持有锁的线程,null 表示锁未被占用 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────┐ ┌─────────────────────────┐ │
│ │ EntryList │ │ WaitSet │ │
│ │ (入口等待队列) │ │ (等待队列) │ │
│ │ │ │ │ │
│ │ ┌─────┐ ┌─────┐ │ │ ┌─────┐ ┌─────┐ │ │
│ │ │线程B│→│线程C│→… │ │ │线程D│→│线程E│→… │ │
│ │ └─────┘ └─────┘ │ │ └─────┘ └─────┘ │ │
│ │ BLOCKED 状态 │ │ WAITING 状态 │ │
│ └─────────────────────┘ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 计数器 (Count) │ │
│ │ 记录 Owner 线程重入次数(可重入锁的实现基础) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
2.2.4 Monitor 的工作原理
场景:三个线程竞争同一把锁
synchronized (lock) {
// 临界区代码
}
详细执行流程:
初始状态:
Owner = null
Count = 0
EntryList = []
WaitSet = []
步骤1:线程A 尝试获取锁
1. 检查 Owner == null → 锁空闲
2. CAS 设置 Owner = 线程A
3. Count++ → Count = 1
4. 线程A 进入临界区
步骤2:线程B 尝试获取锁
1. 检查 Owner != null(被线程A持有)
2. 线程B 无法获取锁
3. 线程B 进入 EntryList(BLOCKED 状态)
4. 线程B 被挂起(不占用 CPU)
步骤3:线程C 尝试获取锁
1. 同样无法获取
2. 进入 EntryList(排在 B 后面)
此时状态:
Owner = 线程A
Count = 1
EntryList = [线程B, 线程C]
WaitSet = []
步骤4:线程A 在临界区中调用 lock.wait()
1. 将线程A 从 Owner 移出
2. 将线程A 放入 WaitSet(WAITING 状态)
3. Count = 0
4. 唤醒 EntryList 中的第一个线程(线程B)
步骤5:线程B 获得锁
1. Owner = 线程B
2. Count = 1
步骤6:线程B 执行 lock.notify()
1. 从 WaitSet 中移出一个线程(线程A)
2. 将线程A 放入 EntryList
3. 线程A 状态从 WAITING → BLOCKED
步骤7:线程B 退出 synchronized 块
1. Count– → Count = 0
2. Owner = null
3. 唤醒 EntryList 中的线程(线程A)
步骤8:线程A 重新竞争锁
1. 获取锁成功
2. Owner = 线程A
3. Count = 1
2.2.5 为什么这是可重入锁?
public synchronized void methodA() {
// 已经持有锁
methodB(); // 可以再次获取同一把锁
}
public synchronized void methodB() {
// 再次获取同一把锁
}
可重入的实现:
当线程A 已经持有锁时再次请求同一把锁:
2.3 字节码层面的 synchronized
2.3.1 反编译 synchronized 代码块
public class SyncExample {
public void test() {
synchronized(this) {
System.out.println("hello");
}
}
}
反编译后的字节码(javap -c SyncExample):
public void test();
Code:
0: aload_0 // 将 this 推入栈顶
1: dup // 复制栈顶(用于 monitorenter)
2: monitorenter // 进入同步块,获取锁
3: getstatic #2 // 获取 System.out
6: ldc #3 // 加载字符串 "hello"
8: invokevirtual #4 // 调用 println
11: aload_0 // 加载 this
12: monitorexit // 退出同步块,释放锁(正常)
13: goto 21 // 跳转到返回
16: aload_0 // 异常处理开始
17: monitorexit // 退出同步块,释放锁(异常)
18: athrow // 重新抛出异常
19: return // 返回
关键点:
- monitorenter 和 monitorexit 成对出现
- 有两个 monitorexit:正常退出和异常退出
- 确保任何情况下都会释放锁
2.3.2 反编译 synchronized 方法
public class SyncMethod {
public synchronized void test() {
System.out.println("hello");
}
}
反编译结果:
public synchronized void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 注意这个标志位
Code:
0: getstatic #2
3: ldc #3
5: invokevirtual #4
8: return
关键点:
- 方法没有 monitorenter/monitorexit 指令
- 使用 ACC_SYNCHRONIZED 标志位
- JVM 看到这个标志,会自动进行加锁/解锁
2.4 锁升级过程(JDK 1.6+ 最重要的优化)
2.4.1 为什么需要锁升级?
问题:早期的 synchronized 直接使用重量级锁(操作系统互斥量)
开销:
- 每次加锁/解锁都要切换到内核态
- 系统调用耗时约 100ns-1μs
- 线程阻塞/唤醒涉及上下文切换(1-10μs)
优化思路:
- 大部分时候,锁的竞争不激烈
- 可以用更轻量的方式处理低竞争场景
- 只在必要时才升级为重量级锁
2.4.2 四种锁状态详解
锁升级方向(单向,不可降级):
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
状态一:无锁
特征:
- 对象刚创建时的状态
- Mark Word 存储 hashCode、age 等信息
- 没有线程持有锁
何时发生:对象创建后,尚未有任何线程尝试获取锁
状态二:偏向锁(Biased Locking)
核心思想:如果一个线程多次获取同一把锁,让该线程"偏向"于这个锁
适用场景:同一个线程反复获取锁(无竞争)
工作流程:
第一次获取锁:
1. 线程A 尝试获取锁
2. Mark Word 为空(无锁状态)
3. CAS 将线程A的ID写入 Mark Word
4. 设置偏向锁标志位(01)
5. 成功获取锁
第二次及以后获取锁:
1. 线程A 再次尝试获取锁
2. 检查 Mark Word 中的线程ID == 当前线程ID
3. 直接进入,不需要任何 CAS 操作
4. 耗时极短(仅需比较几个字节)
对象头变化:
无锁状态:
┌──────────────────────────────────────┐
│ hashcode(25) | age(4) | 0 | 01 │ ← 01 表示无锁/偏向锁未偏向
└──────────────────────────────────────┘
偏向锁(已偏向线程A):
┌──────────────────────────────────────┐
│ 线程ID(23) | epoch(2) | age(4) | 1 | 01
└──────────────────────────────────────┘
↑
线程A的ID
撤销偏向锁:
- 当另一个线程(线程B)尝试获取锁时
- 触发偏向锁撤销(Safe Point 操作)
- 升级为轻量级锁
Java 9+ 默认开启偏向锁,但可通过 JVM 参数关闭:
-XX:-UseBiasedLocking # 关闭偏向锁
状态三:轻量级锁(Lightweight Locking)
核心思想:使用 CAS 自旋,不进行线程阻塞
适用场景:多线程交替执行,短时间内有竞争但很快释放
工作流程:
// 步骤1:线程A 请求锁时发现是偏向锁但被线程B持有
// 步骤2:撤销偏向锁,升级为轻量级锁
线程A:
1. 在栈帧中创建 Lock Record(锁记录)
2. 尝试 CAS:将 Mark Word 复制到 Lock Record
3. CAS 将 Mark Word 指向 Lock Record
4. 如果成功 → 获取锁
5. 如果失败 → 进入自旋
线程B:
1. 执行完临界区
2. CAS 将 Mark Word 恢复原值
3. 释放锁
线程A(自旋中):
1. 看到锁被释放
2. 重新尝试获取
对象头变化:
轻量级锁(线程A持有):
┌──────────────────────────────────────┐
│ 指向 Lock Record 的指针 │ ← 00 表示轻量级锁
└──────────────────────────────────────┘
↓
线程A的栈帧
Lock Record
状态四:重量级锁(Heavyweight Locking)
核心思想:线程阻塞,由操作系统调度
适用场景:竞争非常激烈,自旋超过阈值
触发条件:
- 自旋次数超过阈值(默认 10 次,可调节)
- 有线程在等待(EntryList 不为空)
- JVM 动态判断(自适应自旋)
对象头变化:
重量级锁:
┌──────────────────────────────────────┐
│ 指向 Monitor 对象的指针 │ ← 10 表示重量级锁
└──────────────────────────────────────┘
↓
Monitor
(刚才详细介绍的结构)
2.4.3 锁升级的完整示例
public class LockUpgradeDemo {
private static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
// 阶段1:无锁 → 偏向锁
Thread.sleep(5000); // JVM 启动延迟,4秒后偏向锁才启用
Thread t1 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程1 获得锁");
}
});
t1.start();
t1.join(); // 等待 t1 结束
// 此时 lock 偏向线程1
// 阶段2:偏向锁 → 轻量级锁
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程2 获得锁(轻量级锁)");
}
});
t2.start();
// t2 尝试获取被线程1偏向的锁 → 撤销偏向锁 → 升级为轻量级锁
// 阶段3:轻量级锁 → 重量级锁
Thread t3 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程3 获得锁");
try { Thread.sleep(1000); } catch (Exception e) {}
}
});
Thread t4 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程4 获得锁");
}
});
t3.start();
Thread.sleep(10); // 确保 t3 先拿到锁
t4.start();
// t4 自旋等待 t3 释放,如果自旋超时 → 升级为重量级锁
}
}
2.4.4 锁升级的优缺点对比
| 偏向锁 | 无 CAS 开销,极快 | 撤销需要 Safe Point | 单线程重复获取 |
| 轻量级锁 | 无系统调用,用户态自旋 | 自旋消耗 CPU | 线程交替执行,锁持有时间短 |
| 重量级锁 | 不占 CPU(阻塞) | 系统调用开销大 | 竞争激烈,锁持有时间长 |
2.4.5 面试追问:为什么不直接从无锁到重量级锁?
答案:
三、ReentrantLock 深度解析
3.1 与 synchronized 的详细对比
| 实现层级 | JVM 内置,C++ 实现 | Java 代码,基于 AQS |
| 锁获取方式 | 自动(进入代码块获取) | 手动调用 lock() |
| 锁释放方式 | 自动(离开代码块释放) | 手动调用 unlock() |
| 可中断等待 | ❌ 不可中断 | ✅ lockInterruptibly() |
| 超时尝试 | ❌ 无法尝试 | ✅ tryLock(1, TimeUnit.SECONDS) |
| 非阻塞尝试 | ❌ 无法尝试 | ✅ tryLock() 立即返回 |
| 公平锁支持 | ❌ 只有非公平 | ✅ 构造参数指定 |
| 条件队列 | 1 个(wait/notify) | 多个 Condition |
| 锁信息可见 | 无法查看 | 可查看是否锁、队列长度等 |
| 性能 | JDK 1.6+ 优化后相近 | 灵活场景下略优 |
| 代码简洁 | 简洁 | 需要 try-finally |
3.2 ReentrantLock 完整用法示例
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock(true); // 公平锁
private int count = 0;
// 基本用法
public void basicUsage() {
lock.lock(); // 获取锁,不可中断
try {
count++; // 临界区
} finally {
lock.unlock(); // 必须手动释放
}
}
// 尝试获取锁(非阻塞)
public boolean tryLockUsage() {
if (lock.tryLock()) { // 立即返回,不等待
try {
// 获取锁成功,执行操作
count++;
return true;
} finally {
lock.unlock();
}
} else {
// 获取锁失败,做其他事情
System.out.println("锁被占用,稍后再试");
return false;
}
}
// 超时尝试
public boolean tryLockWithTimeout() throws InterruptedException {
if (lock.tryLock(1, TimeUnit.SECONDS)) { // 等待1秒
try {
// 执行操作
return true;
} finally {
lock.unlock();
}
}
return false;
}
// 可中断获取锁
public void interruptibleLock() throws InterruptedException {
lock.lockInterruptibly(); // 可响应中断
try {
// 执行长时间操作
Thread.sleep(10000);
} finally {
lock.unlock();
}
}
// 锁信息查看
public void inspectLock() {
System.out.println("是否被锁:" + lock.isLocked());
System.out.println("持有锁的线程:" + lock.getOwner());
System.out.println("队列中线程数:" + lock.getQueueLength());
System.out.println("是否公平锁:" + lock.isFair());
System.out.println("当前线程是否持有锁:" + lock.isHeldByCurrentThread());
}
}
3.3 公平锁 vs 非公平锁深入分析
3.3.1 非公平锁(默认)
工作方式:新来的线程直接尝试获取锁,不管队列中是否有等待线程。
// 非公平锁的 lock() 实现(简化)
final void lock() {
if (compareAndSetState(0, 1)) // 先直接抢一次
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 抢不到再去排队
}
优点:
- 吞吐量高(减少了线程唤醒的开销)
- 避免"队首线程"被唤醒后发现锁又被抢走的无效唤醒
缺点:
- 可能造成"线程饥饿"(某些线程长时间得不到锁)
- 理论上存在不公平性
3.3.2 公平锁
工作方式:严格按请求顺序,新线程必须排队。
// 公平锁的 lock() 实现(简化)
final void lock() {
acquire(1); // 直接去排队,不尝试抢占
}
protected final boolean tryAcquire(int acquires) {
if (getState() == 0) {
// 关键:检查队列中是否有等待的线程
if (!hasQueuedPredecessors() && // 没有前驱才尝试
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
}
// 可重入逻辑
return false;
}
优点:
- 避免线程饥饿
- 可预测的执行顺序
缺点:
- 吞吐量较低(约非公平锁的 1/10 – 1/5)
- 更多的上下文切换
3.3.3 性能对比测试
// 测试代码框架
public class FairnessTest {
private static final int THREAD_COUNT = 10;
private static final int OPERATIONS = 1_000_000;
@Test
public void testNonfair() {
ReentrantLock lock = new ReentrantLock(false);
long start = System.nanoTime();
// 执行测试…
System.out.println("非公平锁耗时:" + (System.nanoTime() – start) / 1_000_000 + "ms");
}
@Test
public void testFair() {
ReentrantLock lock = new ReentrantLock(true);
long start = System.nanoTime();
// 执行测试…
System.out.println("公平锁耗时:" + (System.nanoTime() – start) / 1_000_000 + "ms");
}
}
// 典型结果(10线程,100万次操作):
// 非公平锁耗时:约 200ms
// 公平锁耗时:约 800ms (慢 4 倍)
结论:除非需要严格的公平性,否则优先使用非公平锁。
3.4 Condition 深度解析(重要)
3.4.1 为什么需要 Condition?
问题:wait/notify 只有一个等待队列,无法区分不同的等待条件。
例子:生产者-消费者模式
- 生产者需要等待"队列未满"
- 消费者需要等待"队列非空"
- 如果用 wait/notify,生产者可能唤醒生产者,消费者可能唤醒消费者(无效唤醒)
解决方案:多个 Condition 队列
3.4.2 Condition 的完整实现
public class BoundedBuffer {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 不满条件
private final Condition notEmpty = lock.newCondition(); // 不空条件
private final Object[] items = new Object[100];
private int putptr, takeptr, count;
// 生产者
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
System.out.println("队列满,生产者等待");
notFull.await(); // 等待不满
}
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
count++;
System.out.println("生产:" + x + ",当前数量:" + count);
notEmpty.signal(); // 唤醒一个消费者
} finally {
lock.unlock();
}
}
// 消费者
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
System.out.println("队列空,消费者等待");
notEmpty.await(); // 等待不空
}
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
count—;
System.out.println("消费:" + x + ",当前数量:" + count);
notFull.signal(); // 唤醒一个生产者
return x;
} finally {
lock.unlock();
}
}
}
使用示例:
BoundedBuffer buffer = new BoundedBuffer();
// 生产者线程
new Thread(() -> {
for (int i = 0; i < 200; i++) {
try {
buffer.put("item-" + i);
Thread.sleep(10);
} catch (InterruptedException e) {}
}
}).start();
// 消费者线程
new Thread(() -> {
for (int i = 0; i < 200; i++) {
try {
buffer.take();
Thread.sleep(50); // 消费慢,会导致队列满
} catch (InterruptedException e) {}
}
}).start();
3.4.3 Condition 的方法详解
| await() | 等待,可中断 | wait() |
| awaitUninterruptibly() | 等待,不可中断 | – |
| awaitNanos(long nanos) | 超时等待(纳秒) | wait(long timeout) |
| awaitUntil(Date deadline) | 等待到指定时间 | – |
| signal() | 唤醒一个等待线程 | notify() |
| signalAll() | 唤醒所有等待线程 | notifyAll() |
3.4.4 await() 的内部原理
// Condition.await() 的简化流程
public final void await() throws InterruptedException {
// 1. 创建 Node 并加入等待队列
Node node = addConditionWaiter();
// 2. 完全释放锁(支持重入,释放全部 count)
int savedState = fullyRelease(node);
// 3. 阻塞当前线程
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if (Thread.interrupted()) throw new InterruptedException();
}
// 4. 被 signal 后,重新竞争锁
acquireQueued(node, savedState);
}
3.4.5 signal() 的内部原理
// Condition.signal() 的简化流程
public final void signal() {
// 1. 检查当前线程是否持有锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 2. 从等待队列取第一个 Node
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
// 将 Node 从 Condition 队列移到 AQS 队列
if (transferForSignal(first)) {
// 继续处理下一个
}
}
四、AQS 源码级解析
4.1 AQS 的核心组成
AbstractQueuedSynchronizer 是 Java 并发包的基石。
public abstract class AbstractQueuedSynchronizer {
// 1. 核心状态(volatile 保证可见性)
private volatile int state;
// 2. CLH 队列的头尾指针
private transient volatile Node head;
private transient volatile Node tail;
// 3. 内部 Node 类
static final class Node {
volatile Node prev; // 前驱
volatile Node next; // 后继
volatile Thread thread; // 等待的线程
volatile int waitStatus; // 等待状态
Node nextWaiter; // 下一个等待者(Condition 用)
}
// 4. 条件队列
public class ConditionObject implements Condition {
private Node firstWaiter;
private Node lastWaiter;
}
}
4.2 CLH 队列的详细结构
初始状态(head 指向一个空节点):
head → [Node: thread=null, status=0] ← tail
加入线程A(获取锁失败):
head → [哨兵节点] ←→ [Node: thread=A, status=-1] ← tail
↓
(已获得锁的线程)
加入线程B(继续失败):
head → [哨兵] ←→ [Node: A, status=-1] ←→ [Node: B, status=-1] ← tail
线程A释放锁后(被唤醒):
head → [Node: A, status=0] ← tail (A 成为新的头节点)
↓
原本的哨兵节点被 GC
4.3 Node 的 waitStatus 详解
| CANCELLED | 1 | 线程已取消(超时或中断) |
| SIGNAL | -1 | 后继线程需要被唤醒 |
| CONDITION | -2 | 在 Condition 队列中 |
| PROPAGATE | -3 | 共享模式下传播 |
| INITIAL | 0 | 初始状态 |
4.4 独占锁的获取流程(acquire)
// 独占锁获取的模板方法
public final void acquire(int arg) {
// 1. tryAcquire:尝试获取(子类实现)
// 2. addWaiter:失败则加入队列
// 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); // 自旋入队
return node;
}
// 在队列中等待
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; // 帮助 GC
failed = false;
return interrupted;
}
// 检查是否需要阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
4.5 独占锁的释放流程(release)
public final boolean release(int arg) {
if (tryRelease(arg)) { // 子类实现,释放锁
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); // 重置状态
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); // 唤醒线程
}
4.6 ReentrantLock 的 FairSync 实现
static final class FairSync extends Sync {
// 公平锁的 tryAcquire
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;
}
}
// 检查队列中是否有等待的线程
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 条件:头节点 != 尾节点 且 (头节点的下一个为空 或 下一个的线程不是当前线程)
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
五、CAS 完整解析
5.1 CAS 的硬件支持
CAS 不是 Java 的特性,而是 CPU 指令级别的支持。
x86 平台:lock cmpxchg 指令
; 伪汇编代码
lock cmpxchg [dest], src
; lock 前缀:锁定内存总线,确保原子性
; cmpxchg:比较并交换
ARM 平台:LDREX/STREX 指令对
loop:
LDREX r0, [addr] ; 加载并标记独占
CMP r0, r1 ; 比较
BNE exit
STREX r2, r3, [addr] ; 条件存储
CMP r2, #0
BNE loop ; 失败则重试
5.2 Java 中的 CAS 实现(Unsafe 类)
// sun.misc.Unsafe 的核心 CAS 方法
public final native boolean compareAndSwapObject(
Object obj, long offset, Object expect, Object update
);
public final native boolean compareAndSwapInt(
Object obj, long offset, int expect, int update
);
public final native boolean compareAndSwapLong(
Object obj, long offset, long expect, long update
);
// AtomicInteger 的实现
public class AtomicInteger {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}
5.3 自旋锁的实现
// 简单的自旋锁
public class SimpleSpinLock {
private AtomicReference<Thread> owner = new AtomicReference<>();
public void lock() {
Thread current = Thread.currentThread();
// 自旋直到设置成功
while (!owner.compareAndSet(null, current)) {
// 空循环,忙等待
Thread.yield(); // 谦让一下,避免过度消耗 CPU
}
}
public void unlock() {
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}
// 带自适应退避的自旋锁
public class AdaptiveSpinLock {
private AtomicBoolean locked = new AtomicBoolean(false);
private int spinCount = 100; // 初始自旋次数
public void lock() {
int spins = spinCount;
while (true) {
if (locked.compareAndSet(false, true)) {
// 获取锁成功,记录自旋次数(动态调整)
spinCount = Math.min(1000, spins + 10);
return;
}
if (spins > 0) {
spins—;
Thread.onSpinWait(); // Java 9+ 的提示指令
} else {
Thread.yield(); // 自旋失败,让出 CPU
}
}
}
}
5.4 ABA 问题的完整分析
5.4.1 问题复现
// ABA 问题示例
public class ABAProblem {
private static AtomicInteger atomicInt = new AtomicInteger(100);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
// 期望值是 100,改成 101
atomicInt.compareAndSet(100, 101);
// 改回 100
atomicInt.compareAndSet(101, 100);
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(100); // 确保 t1 先执行
} catch (InterruptedException e) {}
// 期望值是 100,改成 200
boolean success = atomicInt.compareAndSet(100, 200);
// success 为 true!t2 不知道值被改过两次
System.out.println("修改成功:" + success);
});
t1.start();
t2.start();
}
}
5.4.2 ABA 的真实危害场景(链表操作)
// 栈结构(Top → NodeA → NodeB → NodeC)
// 线程1:准备将 Top 从 NodeA 改为 NodeB
// 步骤:Node A = top; Node B = A.next; CAS(top, A, B)
// 线程2:在 CAS 之前执行
// 步骤1:pop A(Top 变为 B)
// 步骤2:pop B(Top 变为 C)
// 步骤3:push A(Top 变为 A,但 A 的 next 变了)
// 线程1 恢复执行
// 检查 top == A 成立,CAS 成功
// 将 top 改为 B,但 B 已经被 pop 了!数据丢失
class Stack {
private AtomicReference<Node> top = new AtomicReference<>();
public void push(Node node) {
Node oldTop;
do {
oldTop = top.get();
node.next = oldTop;
} while (!top.compareAndSet(oldTop, node));
}
public Node pop() {
Node oldTop;
Node newTop;
do {
oldTop = top.get();
if (oldTop == null) return null;
newTop = oldTop.next;
} while (!top.compareAndSet(oldTop, newTop));
return oldTop;
}
}
// ABA 问题可能导致错误的节点被删除
5.4.3 解决方案 1:AtomicStampedReference
// 带版本号的引用
public class AtomicStampedReferenceDemo {
private static AtomicStampedReference<String> ref =
new AtomicStampedReference<>("A", 0);
public static void main(String[] args) {
int[] stampHolder = new int[1];
String value = ref.get(stampHolder);
int stamp = stampHolder[0]; // 获取版本号
// 线程1:A → B → A
ref.compareAndSet("A", "B", stamp, stamp + 1);
ref.compareAndSet("B", "A", stamp + 1, stamp + 2);
// 线程2:期望 A,版本号必须匹配
boolean success = ref.compareAndSet("A", "C", stamp, stamp + 3);
System.out.println("修改成功:" + success); // false,版本号不匹配
}
}
5.4.4 解决方案 2:AtomicMarkableReference
// 带布尔标记的引用(更轻量,只需判断是否被改过)
public class AtomicMarkableReferenceDemo {
private static AtomicMarkableReference<String> ref =
new AtomicMarkableReference<>("A", false);
public static void main(String[] args) {
boolean[] markHolder = new boolean[1];
String value = ref.get(markHolder);
boolean mark = markHolder[0];
// 只需要知道是否被修改过,不需要知道修改次数
ref.compareAndSet("A", "B", mark, !mark);
}
}
六、完整对比总结表
6.1 synchronized vs ReentrantLock
| 实现层级 | JVM(C++) | JDK(Java) |
| 锁获取方式 | 自动 | 手动 |
| 锁释放方式 | 自动 | 手动(必须 finally) |
| 可重入性 | ✅ | ✅ |
| 可中断等待 | ❌ | ✅ lockInterruptibly() |
| 超时尝试 | ❌ | ✅ tryLock(timeout) |
| 非阻塞尝试 | ❌ | ✅ tryLock() |
| 公平锁 | ❌ | ✅ 构造参数 |
| 多条件队列 | ❌ wait/notify | ✅ Condition |
| 锁升级 | ✅ 无锁→偏向→轻量→重量 | ❌ 无(直接 AQS) |
| 性能(低竞争) | 优(可能优于 Lock) | 良 |
| 性能(高竞争) | 良 | 优 |
| 代码简洁度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
6.2 核心概念速查表
| 并发 | 逻辑同时,单核快速切换 |
| 并行 | 物理同时,多核真正并行 |
| Monitor | 对象关联的管程(Owner + EntryList + WaitSet) |
| 偏向锁 | 线程ID 写入对象头,单线程重复获取无 CAS |
| 轻量级锁 | CAS 自旋,不阻塞线程 |
| 重量级锁 | 操作系统互斥量,线程阻塞 |
| AQS | state + CLH 队列,同步器框架 |
| CLH 队列 | 双向 FIFO 等待队列 |
| CAS | CPU 原子指令,比较并交换 |
| ABA | A→B→A,CAS 误判,用版本号解决 |
| 自旋 | 不挂起线程,循环等待 |
| 可重入 | 同一线程可多次获取同一锁 |
| 公平锁 | 按请求顺序获取锁 |
| 非公平锁 | 允许抢占,吞吐量更高 |
| Condition | Lock 的多条件等待队列 |
| volatile | 可见性 + 有序性,不保证原子性 |
希望这篇博客能帮你更好理解线程。如果觉得有用,欢迎点赞收藏!





