1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| 偏向锁 → 轻量级锁 → 重量级锁。根据线程竞争的激烈程度,动态切换锁的类型。所有锁的状态都存储在锁对象的对象头(Mark Word)
1. 偏向锁 —— 无竞争时的优化: 适用场景:只有一个线程反复进入同步块,无其他线程竞争。 核心逻辑:“偏向” 第一个获取锁的线程,后续该线程再次进入时,无需竞争,直接通过 “Mark Word 标记” 确认身份即可,几乎无开销。
(1) 线程 A 第一次进入同步块,JVM 检查对象 Mark Word (对象头)是 “无锁状态”。 (2) 通过 CAS 操作,将 Mark Word 中的 “锁标志位” 设为 1(偏向锁),并记录线程 A 的 ID(存入 Mark Word)。 (3) 后续线程 A 再次进入同步块时,只需对比 Mark Word 中的线程 ID 是否为自己: 是:直接进入(“偏向” 生效,无需 CAS,开销极低)。 否:触发偏向锁撤销,进入轻量级锁流程。 (4) 撤销条件:当有其他线程(如线程 B)尝试获取该锁时,JVM 会暂停线程 A,检查线程 A 是否还在执行同步块: 若线程 A 已退出:将 Mark Word 重置为无锁状态,线程 B 可竞争。 若线程 A 仍在执行:偏向锁撤销,升级为轻量级锁。
2. 轻量级锁 —— 低竞争时的优化 适用场景:多个线程交替进入同步块(无同时竞争,即 “自旋等待” 可解决),避免阻塞线程(阻塞 / 唤醒的开销远大于自旋)。
(1) 加锁过程: 当偏向锁被撤销升级为轻量级锁时,只有持有偏向锁的线程 A会在栈帧中创建锁记录(Lock Record),并通过 CAS 将对象头 Mark Word 指向该锁记录(标志位设为 00) 线程 B 尝试获取锁时,会先在自己的栈帧创建锁记录,然后尝试 CAS 将对象头 Mark Word 指向自己的锁记录 线程B获取到锁进入代码块。此时线程A不会竞争锁
(2)解锁过程修正: 线程 A 释放轻量级锁时,通过 CAS 将对象头恢复为 Displaced Mark Word 若 CAS 成功:表示没有其他线程竞争,直接释放锁 若 CAS 失败:说明线程 B 已触发锁膨胀(此时对象头已指向重量级锁的 monitor),线程 A 会释放重量级锁并唤醒阻塞队列中的线程 B
3. 重量级锁 —— 高竞争时的兜底 适用场景:多个线程同时竞争锁(自旋等待失效,继续自旋会浪费 CPU),此时需要通过操作系统的 “互斥量(Mutex)” 实现线程阻塞与唤醒,保证线程安全。
3.1 获取重量级锁 (1). 检查对象头的锁状态(Mark Word) Java 中每个对象的内存布局包含对象头(Mark Word),其中存储了锁的状态信息。线程首先读取目标对象的 Mark Word: 若 Mark Word 的锁标志位为10(重量级锁的标志位),说明锁已被其他线程占用;
(2). 关联重量级锁结构(ObjectMonitor) JVM 为每个重量级锁维护一个ObjectMonitor(对象监视器) 结构体(C++ 实现),其核心字段包括: _owner:指向当前持有锁的线程; _WaitSet:存储因等待锁而被挂起的线程队列(条件等待队列); _EntryList:存储尝试获取锁但未成功的线程队列(入口等待队列); _recursions:锁的重入次数(避免线程重复加锁导致死锁)。 当锁首次升级为重量级时,JVM 会为对象分配一个 ObjectMonitor,并将 Mark Word 中的 “指针” 指向该 ObjectMonitor。
(3). 尝试获取锁(CAS 竞争) 线程通过CAS 操作尝试修改 ObjectMonitor 的_owner字段: 若_owner为null(锁空闲):CAS 成功,将_owner设为当前线程,_recursions设为 1,加锁成功,线程继续执行临界区代码; 若_owner已指向当前线程(重入场景):直接将_recursions加 1,加锁成功(体现 “可重入锁” 特性); 若_owner指向其他线程(锁被占用):CAS 失败,进入下一步 —— 线程入队等待。
(4). 线程入队(EntryList) CAS 失败的线程会被 JVM 放入 ObjectMonitor 的_EntryList(入口等待队列),此时线程状态从 “RUNNABLE” 变为 “BLOCKED”(阻塞状态)。
(5). 线程挂起(内核态等待) JVM 通过调用操作系统的 **pthread_mutex_lock()** 函数(Linux 系统),将_EntryList中的线程从用户态切换到内核态,挂起线程(不再占用 CPU 时间片),等待锁释放的信号。 这一步是重量级锁开销大的核心原因:用户态与内核态切换需要消耗大量 CPU 资源,且线程挂起 / 唤醒的调度由操作系统内核完成,延迟较高。
(6). 锁释放后的唤醒与重试 当持有锁的线程释放锁后,会唤醒_EntryList中的一个或多个线程(具体唤醒策略由操作系统调度决定,如 FIFO)。被唤醒的线程从内核态切换回用户态,重新尝试执行步骤 3(CAS 竞争锁),重复上述过程直到获取锁。
3.2 释放重量级锁 (1). 检查锁的重入次数(_recursions) 线程首先检查 ObjectMonitor 的_recursions字段: 若_recursions > 1(重入场景):仅将_recursions减 1,释放 “一次重入”,不真正释放锁(避免其他线程提前竞争); 若_recursions == 1:进入真正的锁释放流程。
(2). 重置 ObjectMonitor 的_owner 字段 线程通过 CAS 操作将 ObjectMonitor 的_owner字段设为null,标记锁为 “空闲” 状态。
(3). 唤醒等待队列中的线程 JVM 调用操作系统的 **pthread_mutex_unlock()** 函数(Linux 系统),唤醒 ObjectMonitor 中_EntryList(或_WaitSet,若涉及wait()/notify())中的等待线程: 唤醒策略:通常是 “随机唤醒” 或 “公平唤醒”(取决于 JVM 和操作系统实现,默认非公平); 被唤醒的线程:从 “BLOCKED” 状态变为 “RUNNABLE” 状态,重新参与锁的竞争。
(4). 线程状态恢复与锁竞争重试 被唤醒的线程从内核态切换回用户态,重新尝试获取锁(重复加锁过程的步骤 3): 若此时有多个线程被唤醒,会再次通过 CAS 竞争_owner字段,未竞争到的线程会重新进入_EntryList并挂起; 若锁被其他线程抢先获取,当前线程会再次进入阻塞状态,等待下一次唤醒。
(5). 特殊场景:wait ()/notify () 的影响 若持有锁的线程在临界区中调用了Object.wait()方法,会触发额外逻辑: 线程先释放锁(执行上述释放锁步骤 2-3); 线程从_EntryList转移到_WaitSet(条件等待队列),进入 “WAITING” 状态; 当其他线程调用Object.notify()/notifyAll()时,_WaitSet中的线程会被转移回_EntryList,重新参与锁竞争。
|