欢迎光临
我们一直在努力

从对象头到 AQS:Java 锁机制的底层博弈

从对象头到 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()方法

执行流程:

  • t.run() 是在当前线程(比如 main 线程)的方法调用栈中执行
  • 没有任何新线程被创建
  • 相当于普通的对象方法调用
  • 1.1.2 start() 方法

    start() 方法会创建新线程,并由新线程自动执行 run() 方法。

    // 正确用法
    MyThread t = new MyThread();
    t.start(); // 创建新线程,新线程执行run()方法

    执行流程:

  • start() 方法向 JVM 请求创建一个新的系统线程
  • JVM 分配线程栈、程序计数器等资源
  • 新线程被调度后,自动调用 run() 方法
  • 调用 start() 的线程(如 main)和新线程并行执行
  • 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 中存储了锁信息,根据锁状态不同,解释方式也不同:

    锁状态29 bit(32位JVM)标志位
    无锁 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 已经持有锁时再次请求同一把锁:

  • 检查 Owner == 当前线程(线程A)
  • 不需要重新竞争
  • 执行 Count++ → Count = 2
  • 退出时 Count–,直到 Count = 0 才真正释放

  • 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. CASMark Word 指向 Lock Record
    4. 如果成功 → 获取锁
    5. 如果失败 → 进入自旋

    线程B
    1. 执行完临界区
    2. CASMark 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 面试追问:为什么不直接从无锁到重量级锁?

    答案:

  • 性能考量:90% 的场景没有竞争,偏向锁和轻量级锁足够
  • 减少系统调用:用户态操作远快于内核态
  • 自适应优化:JVM 根据历史竞争情况动态调整
  • 内存效率:锁信息直接存在对象头,不额外分配 Monitor(直到需要时)

  • 三、ReentrantLock 深度解析

    3.1 与 synchronized 的详细对比

    维度synchronizedReentrantLock
    实现层级 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 的方法详解
    方法说明对应 Object
    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

    维度synchronizedReentrantLock
    实现层级 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 可见性 + 有序性,不保证原子性

    希望这篇博客能帮你更好理解线程。如果觉得有用,欢迎点赞收藏!

    赞(0)
    未经允许不得转载:171主机测评 » 从对象头到 AQS:Java 锁机制的底层博弈
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址