14 JVM synchronized实现原理:从对象头到锁升级

图片[1]-14 JVM synchronized实现原理:从对象头到锁升级-速优课

synchronized 是 Java 中最基本的同步机制,用于保证方法或代码块的原子性、可见性和有序性。本文将深入探讨 synchronized 的底层实现原理,包括对象头结构、锁升级机制以及各种锁状态的特点。

一、synchronized 的使用方式

同步方法

public synchronized void method() {
    // 同步代码块
}

同步代码块

public void method() {
    synchronized (this) {
        // 同步代码块
    }
}

类锁

public static synchronized void staticMethod() {
    // 同步代码块
}

二、对象头与锁状态

对象头结构

在 HotSpot 虚拟机中,对象头由两部分组成:

部分大小说明
Mark Word8 字节(64位)存储锁状态、哈希码、分代年龄等
Class Pointer8 字节(64位)指向类的元数据

Mark Word 的锁状态

锁状态锁标志Mark Word 内容
无锁01对象哈希码(25位)+ 分代年龄(4位)+ 是否偏向(1位=0)+ 锁标志(2位=01)
偏向锁01线程 ID(54位)+ epoch(2位)+ 分代年龄(4位)+ 是否偏向(1位=1)+ 锁标志(2位=01)
轻量级锁00指向栈中锁记录的指针(62位)+ 锁标志(2位=00)
重量级锁10指向重量级锁(Monitor)的指针(62位)+ 锁标志(2位=10)
GC 标记11空(62位)+ 锁标志(2位=11)

三、锁升级机制

锁升级流程

无锁 → 偏向锁 → 轻量级锁 → 重量级锁
     ↑                    ↑
     └────────────────────┘
         不可降级

无锁状态

对象刚创建时处于无锁状态:

  • Mark Word 存储对象的哈希码和分代年龄
  • 是否偏向位为 0

偏向锁

触发条件

  • 对象被第一次访问时
  • 只有一个线程访问对象

获取偏向锁

  1. 检查对象的偏向锁标志是否为 1
  2. 如果是,检查线程 ID 是否为当前线程
  3. 如果是,直接执行同步代码
  4. 如果不是,尝试 CAS 获取偏向锁

释放偏向锁

  • 偏向锁不会主动释放
  • 只有在其他线程尝试获取锁时才会撤销

轻量级锁

触发条件

  • 多个线程交替访问对象
  • 没有长时间的锁竞争

获取轻量级锁

  1. 在当前线程的栈帧中创建锁记录(Lock Record)
  2. 将对象的 Mark Word 复制到锁记录中
  3. 使用 CAS 将对象的 Mark Word 指向锁记录
  4. 如果 CAS 成功,获取锁;如果失败,自旋等待

释放轻量级锁

  1. 使用 CAS 将对象的 Mark Word 恢复为锁记录中的值
  2. 如果 CAS 成功,释放锁;如果失败,说明有竞争,升级为重量级锁

重量级锁

触发条件

  • 多个线程长时间竞争锁
  • 轻量级锁自旋次数超过阈值

获取重量级锁

  1. 将对象的 Mark Word 指向 Monitor 对象
  2. 当前线程进入 Monitor 的等待队列
  3. 线程阻塞,等待被唤醒

释放重量级锁

  1. 当前线程释放 Monitor
  2. 唤醒等待队列中的一个线程
  3. 被唤醒的线程尝试获取锁

四、Monitor 对象

Monitor 的结构

Monitor(管程)是重量级锁的核心数据结构:

ObjectMonitor {
    _header       // 对象头
    _count        // 锁计数器(重入次数)
    _waiters      // 等待队列
    _recursions   // 重入次数
    _owner        // 持有锁的线程
    _EntryList    // 入口队列(等待获取锁的线程)
    _WaitSet      // 等待队列(调用 wait() 的线程)
}

Monitor 的工作流程

// 获取锁
monitorenter:
    if (_owner == null) {
        _owner = currentThread;
        _count = 1;
    } else if (_owner == currentThread) {
        _count++;  // 重入
    } else {
        currentThread.block();  // 阻塞
    }
​
// 释放锁
monitorexit:
    _count--;
    if (_count == 0) {
        _owner = null;
        wakeup(_EntryList);  // 唤醒等待线程
    }

五、锁优化技术

自旋锁

在升级为重量级锁之前,线程会自旋等待:

  • 默认自旋次数为 10 次
  • 使用 -XX:PreBlockSpin 参数调整
  • Java 6 引入自适应自旋,根据历史情况动态调整

锁消除

JIT 编译器会消除不必要的锁:

  • 如果锁对象不会逃逸出当前方法
  • 如果锁对象只被一个线程访问
  • 使用 -XX:+EliminateLocks 参数启用

锁粗化

JIT 编译器会合并相邻的锁:

  • 将多次加锁解锁合并为一次
  • 减少锁的开销

偏向锁优化

Java 6 默认启用偏向锁:

  • 使用 -XX:+UseBiasedLocking 参数启用
  • 使用 -XX:BiasedLockingStartupDelay 参数设置延迟时间

六、synchronized 的性能对比

不同锁状态的性能

锁状态获取开销释放开销适用场景
无锁单线程访问
偏向锁极低极低单线程重复访问
轻量级锁多线程交替访问
重量级锁多线程竞争

synchronized vs ReentrantLock

特性synchronizedReentrantLock
锁类型独占锁独占锁
可重入
公平性非公平可配置
可中断
超时获取
条件变量单一多个
性能高(JDK 6+)

七、总结

synchronized 的实现涉及多个层面:

  1. 对象头:Mark Word 存储锁状态信息
  2. 锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
  3. Monitor:重量级锁的核心数据结构
  4. 锁优化:自旋锁、锁消除、锁粗化、偏向锁

关键要点

  • JDK 6 对 synchronized 进行了重大优化,性能大幅提升
  • 锁升级是单向的,不会降级
  • 选择合适的锁类型可以提高程序性能

理解 synchronized 的底层实现,有助于编写更高效的并发代码。

© 版权声明
THE END
喜欢就支持一下吧
点赞9
相关推荐
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容

温馨提示:
1、本内容转载于网络,版权归原作者所有!
2、本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
3、本内容若侵犯到你的版权利益,请联系我们,会尽快给予删除处理!