当前位置: 首页 > article >正文

Java Synchronized底层原理:Monitor机制、锁膨胀、自旋优化与偏向锁细节解密

深入剖析Java Synchronized底层原理:从对象头到锁升级的全面解读

📖 摘要

关键词Java并发Synchronized原理锁升级机制对象头Monitor
本文将深入解析Java中synchronized关键字的底层实现,涵盖对象头结构、Monitor机制、锁升级流程等核心内容,帮助开发者理解高并发场景下的锁优化策略。无论是面试准备还是性能调优,本文都能为你提供清晰的技术洞察!


📑 目录

  1. 引言:为什么需要了解synchronized底层原理?
  2. 对象头与Mark Word的奥秘
  3. Monitor(管程)机制详解
  4. 锁升级:偏向锁→轻量级锁→重量级锁
  5. synchronized的可重入性实现
  6. 总结与面试题

1. 引言:为什么需要了解synchronized底层原理?

在多线程编程中,synchronized是Java开发者最常用的同步工具。但你是否遇到过这些问题?

  • 性能瓶颈:简单粗暴地加锁导致吞吐量下降?
  • 死锁问题:线程互相等待却不知如何排查?
  • 面试必问:如何回答“synchronized和ReentrantLock的区别”?

理解其底层原理,不仅能优化高并发程序,还能深入JVM设计思想,更是大厂面试的核心考点


2. 对象头与Mark Word的奥秘

每个Java对象都暗藏玄机!对象内存布局分为三部分:

在这里插入图片描述
在这里插入图片描述

其中Mark Word是实现锁的关键,其结构随锁状态动态变化(以64位JVM为例):

锁状态存储内容标识位
无锁unused(25bit)、hashcode(31bit)、分代年龄(4bit)等001
偏向锁线程ID(54bit)、Epoch(2bit)、分代年龄(4bit)101
轻量级锁指向栈中锁记录的指针(62bit)00
重量级锁指向Monitor的指针(62bit)10

3. Monitor(管程)机制详解

当使用synchronized时,JVM会通过**Monitor(管程)**实现互斥,其核心结构如下:

class Monitor {
    Thread* owner;          // 持有锁的线程
    EntryList* entrySet;    // 阻塞等待锁的线程队列
    WaitSet* waitSet;       // 调用wait()后的等待队列
    int recursions;         // 重入次数计数器
};

执行流程

  1. 线程通过monitorenter指令尝试获取Monitor所有权。
  2. 若Owner为空,则当前线程成为Owner,recursions=1。
  3. 若Owner是当前线程,recursions++(可重入性)。
  4. 竞争失败则进入EntryList阻塞等待。

Monitor的完整结构:线程如何被管理?
在这里插入图片描述

Monitor是synchronized实现互斥的核心,其完整结构在JVM源码(如objectMonitor.hpp)中定义如下:

class ObjectMonitor {
    void*     _header;        // 对象头(存储Mark Word)
    void*     _owner;         // 持有锁的线程指针
    volatile intptr_t  _recursions; // 重入次数
    ObjectWaiter* _EntryList; // 竞争锁的线程队列(阻塞态)
    ObjectWaiter* _WaitSet;   // 调用wait()后的线程队列(等待态)
    volatile int _WaitSetLock;// 保护WaitSet的锁
    // ... 其他字段省略
};

关键队列说明:

  • EntryList
    当线程A持有锁时,线程B尝试获取锁失败,会被封装为ObjectWaiter节点加入EntryList,并进入BLOCKED状态。
    唤醒规则:锁释放时,JVM会从EntryList中选择线程(非公平模式下可能直接唤醒最新竞争者)。

  • WaitSet
    当线程调用wait()方法后,线程会释放锁并进入WaitSet,状态变为WAITING
    唤醒条件:其他线程调用notify()/notifyAll()后,线程从WaitSet转移到EntryList重新竞争锁。


2. 锁膨胀(Lock Inflation):轻量级锁如何升级为重量级锁?

触发条件:

  • 轻量级锁CAS替换Mark Word失败(多线程竞争)。
  • 自旋锁尝试超过阈值(JDK6后为自适应自旋)。

cas替换
在这里插入图片描述
替换成功
在这里插入图片描述

膨胀流程(源码级解析):

1. 线程A持有轻量级锁(Mark Word指向栈中Lock Record)
2. 线程B竞争锁,CAS失败 → 开始自旋
3. 自旋失败 → JVM准备膨胀为重量级锁
4. 创建ObjectMonitor对象,初始化Owner、EntryList等
5. 线程A释放轻量级锁时,发现锁已膨胀:
   a. 修改Mark Word为指向ObjectMonitor的指针(锁标志位10)
   b. 唤醒EntryList中的线程B进入锁竞争
6. 后续所有锁操作直接通过ObjectMonitor进行

重量级锁的核心代价:

  • 上下文切换:线程竞争失败后从用户态陷入内核态,由操作系统调度阻塞与唤醒。
  • 吞吐量下降:适合高竞争但线程执行时间长的场景(如数据库连接池争抢)。

3. 自旋优化(Spin Locking):竞争时的CPU空转策略

自旋锁的意义:

  • 减少上下文切换:线程不立即阻塞,而是循环尝试获取锁(忙等待)。
  • 适用场景:锁竞争时间短、线程数少(如单核CPU或低并发)。

JDK的自适应自旋优化(Adaptive Spinning):

  • 动态调整:根据上次自旋成功次数动态决定本次自旋时间。
  • 实现逻辑
    1. 若上次自旋成功获得锁,则允许更长的自旋时间。
    2. 若自旋很少成功,则直接跳过自旋进入阻塞。

自旋示例代码(伪代码):

void EnterSpinLock() {
    int spins = 0;
    while (!tryLock() && spins < max_spins) {
        ++spins;
        Thread::SpinWait();  // CPU空转(如执行PAUSE指令)
    }
    if (!tryLock()) {
        EnterBlockingQueue(); // 加入EntryList并阻塞
    }
}

4. 锁升级:偏向锁→轻量级锁→重量级锁

锁升级是JVM优化性能的关键策略!流程图解:

          ┌───────────┐
          │   无锁     │
          └─────┬─────┘
                │ 首次访问
          ┌─────▼─────┐
          │  偏向锁    │◄───┐
          └─────┬─────┘    │同一线程多次访问
                │ 竞争发生  │
          ┌─────▼─────┐    │
          │ 轻量级锁  │───►┘
          └─────┬─────┘
                │ CAS失败
          ┌─────▼─────┐
          │ 重量级锁  │
          └───────────┘

4.1 偏向锁(Biased Lock)

  • 场景:单线程重复访问同步块。
  • 原理:Mark Word中记录线程ID,后续无需CAS。
  • 示例:Web服务中用户独享资源的处理。

4.2 轻量级锁(Lightweight Lock)

  • 场景:低竞争的多线程环境。
  • 原理
    • 线程栈中创建Lock Record,拷贝Mark Word。
    • 通过CAS将对象头指向Lock Record。
  • 失败:触发自旋锁(JDK6前默认10次,后改为自适应)。

4.3 重量级锁(Heavyweight Lock)

  • 场景:高并发激烈竞争。
  • 原理:依赖OS的Mutex Lock,线程进入阻塞状态。
  • 代价:用户态到内核态的上下文切换。

JDK 15后偏向锁的默认禁用原因:

  • 维护成本高:撤销操作需STW(Stop-The-World),影响GC暂停时间。
  • 收益下降:现代应用多线程竞争普遍,偏向锁适用场景减少。
  • 禁用方式:通过JVM参数-XX:-UseBiasedLocking关闭。

5. synchronized的可重入性实现

通过Monitor的recursions计数器实现:

public class ReentrantDemo {
    public synchronized void methodA() {
        methodB(); // 可重入
    }
    
    public synchronized void methodB() {
        // 计数器递增至2
    }
}
  • 每个锁操作对应recursions++,退出时recursions--
  • recursions=0时,锁完全释放。

6. 总结与面试题

核心总结表:

机制优势劣势适用场景
偏向锁单线程零成本加锁撤销开销大明确单线程访问
轻量级锁CAS无阻塞竞争自旋消耗CPU低竞争、短临界区
重量级锁高竞争下稳定上下文切换开销高并发、长临界区

1. 锁膨胀是可逆的吗?

不可逆。一旦升级为重量级锁,无法降级,但偏向锁可重置为无锁。

2. 为什么wait()必须在synchronized块中调用?

wait()会释放锁,需先获取Monitor的所有权(否则抛出IllegalMonitorStateException)。

3. 自旋锁是否一定优于阻塞?

不一定。长时间自旋浪费CPU,需根据竞争激烈程度选择。

4.synchronized vs ReentrantLock对比

特性synchronizedReentrantLock
锁实现JVM内置,自动释放API层面,需手动unlock()
中断响应不支持支持lockInterruptibly()
公平锁非公平可配置公平策略
条件队列单一wait/notify支持多个Condition
性能JDK6后优化后接近高竞争场景更灵活

http://www.kler.cn/a/613011.html

相关文章:

  • 电气技术:未来自动化的心脏
  • RAG生成中的多文档动态融合及去重加权策略探讨
  • springboot 四层架构之间的关系整理笔记二
  • 【CSS3】02-选择器 + CSS特性 + 背景属性 + 显示模式
  • 硬件老化测试方案的设计误区
  • sock文件介绍--以mysql.sock为例
  • torchvision中数据集的使用
  • 基于神经网络的文本分类的设计与实现
  • 告别代码Bug,GDB调试工具详解
  • 使用Selenium和lxml库搜房网爬取某地区房屋信息(python、pycharm爬虫)
  • 某投行日志记录解决方案二之日志异步落盘: 自定义注解+反射+AOP+异步多线程,实现高并发场景下的统一日志治理方案
  • 94二叉树中序遍历解题记录
  • SpringCloud-环境和工程搭建
  • 基于SpringBoot + Vue 的考勤管理系统
  • 浅谈数据结构
  • CSS FLEX布局
  • 解决 “Cannot read SQL script from class path resource [sql/XX.sql]“ 错误
  • 【科研绘图系列】R语言绘制重点物种进化树图(taxa phylogenetic tree)
  • 微服务面试题:配置中心
  • 基于大模型的自发性气胸全方位预测与诊疗方案研究