一、synchronized 可重入的原理
1. 什么是可重入锁?
- 可重入:同一个线程可以多次获取同一把锁,而不会产生死锁。
- synchronized:Java 中的内置锁,支持可重入。
2. 可重入的实现机制
-
锁对象头:每个对象都有一个对象头,其中包含锁的状态信息。
-
锁计数器:synchronized 通过锁计数器记录线程获取锁的次数。
- 每次获取锁时,计数器加 1。
- 每次释放锁时,计数器减 1。
- 当计数器为 0 时,锁完全释放。
二、synchronized 的底层数据结构
1. 对象头(Mark Word)
-
组成:对象头包含以下信息:
- 锁状态(无锁、偏向锁、轻量级锁、重量级锁)。
- 锁计数器(记录重入次数)。
- 线程 ID(记录持有锁的线程)。
2. 锁状态变化
| 锁状态 | 描述 |
|---|---|
| 无锁 | 对象未被锁定。 |
| 偏向锁 | 只有一个线程访问锁时,JVM 会偏向该线程,减少锁操作的开销。 |
| 轻量级锁 | 多个线程竞争锁时,JVM 会将锁升级为轻量级锁,通过自旋尝试获取锁。 |
| 重量级锁 | 竞争激烈时,JVM 会将锁升级为重量级锁,线程进入阻塞状态,等待锁释放。 |
三、synchronized 可重入的底层数据变化
1. 获取锁时的数据变化
-
步骤:
-
检查锁状态:
- 如果是无锁状态,直接获取锁,锁计数器加 1。
- 如果是偏向锁或轻量级锁,检查持有锁的线程是否为当前线程。
-
如果是当前线程,锁计数器加 1。
-
如果不是当前线程,尝试升级锁状态(如从偏向锁升级为轻量级锁)。
-
-
数据变化:
- 锁计数器加 1。
- 线程 ID 更新为当前线程(如果是无锁状态)。
2. 释放锁时的数据变化
-
步骤:
- 锁计数器减 1。
- 如果锁计数器为 0,释放锁,锁状态恢复为无锁。
-
数据变化:
- 锁计数器减 1。
- 线程 ID 清除(如果锁计数器为 0)。
四、代码示例与底层数据变化
1. 代码示例
public class ReentrantDemo {
public synchronized void method1() {
System.out.println("进入 method1");
method2(); // 调用 method2
}
public synchronized void method2() {
System.out.println("进入 method2");
}
public static void main(String[] args) {
ReentrantDemo demo = new ReentrantDemo();
demo.method1(); // 输出:进入 method1\n进入 method2
}
}
2. 底层数据变化
步骤 1:第一次获取锁
-
锁状态:无锁 → 偏向锁。
-
数据变化:
- 锁计数器:0 → 1。
- 线程 ID:null → 当前线程 ID。
步骤 2:第二次获取锁
-
锁状态:偏向锁(当前线程持有锁)。
-
数据变化:
- 锁计数器:1 → 2。
步骤 3:第一次释放锁
-
锁状态:偏向锁。
-
数据变化:
- 锁计数器:2 → 1。
步骤 4:第二次释放锁
-
锁状态:偏向锁 → 无锁。
-
数据变化:
- 锁计数器:1 → 0。
- 线程 ID:当前线程 ID → null。
五、Summary
-
synchronized 可重入:通过锁计数器实现,同一个线程可以多次获取同一把锁。
-
底层数据变化:
- 获取锁时,锁计数器加 1,线程 ID 更新。
- 释放锁时,锁计数器减 1,线程 ID 清除。