synchronized 可重入原理与底层数据变化详解

117 阅读3分钟

一、synchronized 可重入的原理

1. 什么是可重入锁?

  • 可重入:同一个线程可以多次获取同一把锁,而不会产生死锁。
  • synchronized:Java 中的内置锁,支持可重入。

2. 可重入的实现机制

  • 锁对象头:每个对象都有一个对象头,其中包含锁的状态信息。

  • 锁计数器:synchronized 通过锁计数器记录线程获取锁的次数。

    • 每次获取锁时,计数器加 1。
    • 每次释放锁时,计数器减 1。
    • 当计数器为 0 时,锁完全释放。

二、synchronized 的底层数据结构

1. 对象头(Mark Word)

  • 组成:对象头包含以下信息:

    • 锁状态(无锁、偏向锁、轻量级锁、重量级锁)。
    • 锁计数器(记录重入次数)。
    • 线程 ID(记录持有锁的线程)。

2. 锁状态变化

锁状态描述
无锁对象未被锁定。
偏向锁只有一个线程访问锁时,JVM 会偏向该线程,减少锁操作的开销。
轻量级锁多个线程竞争锁时,JVM 会将锁升级为轻量级锁,通过自旋尝试获取锁。
重量级锁竞争激烈时,JVM 会将锁升级为重量级锁,线程进入阻塞状态,等待锁释放。

三、synchronized 可重入的底层数据变化

1. 获取锁时的数据变化

  • 步骤

    1. 检查锁状态:

      • 如果是无锁状态,直接获取锁,锁计数器加 1。
      • 如果是偏向锁或轻量级锁,检查持有锁的线程是否为当前线程。
    2. 如果是当前线程,锁计数器加 1。

    3. 如果不是当前线程,尝试升级锁状态(如从偏向锁升级为轻量级锁)。

  • 数据变化

    • 锁计数器加 1。
    • 线程 ID 更新为当前线程(如果是无锁状态)。

2. 释放锁时的数据变化

  • 步骤

    1. 锁计数器减 1。
    2. 如果锁计数器为 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 清除。