从零起步学习并发编程 || 第四章:synchronized底层源码级讲解及项目实战应用案例

一、synchronized 是什么?

synchronizedJava 语言原生提供的内置同步关键字 ,属于可重入的独占悲观锁,由 HotSpot JVM 底层(C++)实现,是解决多线程并发安全问题最基础、最常用的方案。

它的核心特性:

  1. 独占性:同一时间,只有一个线程能持有锁并执行被修饰的代码;
  2. 可重入性:持有锁的线程可以再次进入同一把锁修饰的代码块,不会发生死锁;
  3. JVM 自动管理:加锁、解锁、异常释放锁均由虚拟机自动完成,无需手动编码,降低使用门槛;
  4. 绑定对象锁:锁的载体是 Java 对象(实例对象 / Class 对象),每个对象都关联一个监视器锁(Monitor)。

二、synchronized 有什么用?

多线程并发访问共享资源 (成员变量、静态变量、公共对象等)时,会出现线程安全问题 (原子性、可见性、有序性问题),synchronized 的核心作用就是保证并发场景下的线程安全,具体解决三大问题:

问题类型 定义 synchronized 如何解决
原子性 一个操作不可分割,要么全部执行成功,要么全部不执行 加锁后只有单线程执行同步代码,杜绝并发交叉执行
可见性 一个线程修改共享变量后,其他线程能立即感知到最新值 解锁时会将工作内存数据刷新到主内存,加锁时从主内存重新读取数据
有序性 禁止指令重排序,保证代码执行顺序符合编写逻辑 基于内存屏障机制,禁止锁内代码重排,保证执行有序

典型应用场景

  • 电商场景:库存扣减、订单创建,避免超卖、重复下单;
  • 计数场景:多线程统计访问量、点赞数,避免数值计算错误;
  • 共享对象操作:多线程修改集合、公共配置对象,保证数据一致性。

三、synchronized 的三种使用方式

根据锁的目标对象不同,分为三种用法,锁的范围和作用对象完全不同,这是开发中最容易出错的点:

1. 修饰实例方法

锁对象:当前实例对象(this),不同实例对象拥有独立的锁,互不干扰。

java 复制代码
public class Stock {
    // 共享资源:库存
    private int count = 100;

    // 修饰实例方法,锁当前this对象
    public synchronized void deductStock() {
        if (count > 0) {
            count--;
            System.out.println("库存剩余:" + count);
        }
    }
}
2. 修饰静态方法

锁对象:当前类的 Class 对象,全局唯一,所有实例共享同一把锁,同步范围最大。

java 复制代码
public class Order {
    // 静态共享资源
    private static int orderNum = 10000;

    // 修饰静态方法,锁 Order.class 对象
    public static synchronized void createOrder() {
        orderNum++;
        System.out.println("生成订单号:" + orderNum);
    }
}
3. 修饰同步代码块(推荐)

锁对象:自定义指定对象 (推荐使用 final 修饰的锁对象),可以灵活控制锁的粒度,性能最优。

java 复制代码
public class Demo {
    // 自定义锁对象,final 防止对象引用被修改导致锁失效
    private final Object lock = new Object();
    private int num = 0;

    public void addNum() {
        // 同步代码块,仅锁定核心逻辑,缩小锁范围
        synchronized (lock) {
            num++;
        }
    }
}
  • 达到阈值(JVM 自适应调整)仍未获取锁;
  • 出现第三个线程竞争锁(自旋会造成 CPU 空耗,触发升级)。

四、锁升级全流程与 synchronized 的底层实现

Java 对象头是锁升级的核心载体,对象头中的Mark Word字段会存储锁状态标识、线程 ID、锁指针等关键信息,不同锁状态对应不同的 Mark Word 结构。整个升级流程分为四个阶段,对应 synchronized 的四种工作模式:

阶段 1:无锁状态

  • 场景:没有任何线程竞争同步资源
  • 特点:对象头 Mark Word 标记为无锁,线程可以自由访问资源,无需加锁
  • 这是 synchronized 加锁前的初始状态

阶段 2:偏向锁(Biased Lock)

适用场景

只有单个线程反复访问同步代码块,无多线程竞争,是锁升级的第一个状态。

工作原理
  1. 当第一个线程访问 synchronized 代码块时,JVM 通过 CAS 操作将线程 ID写入对象头的 Mark Word 中,同时将锁标记改为偏向锁;
  2. 该线程后续再次进入同步块时,只需比对 Mark Word 中的线程 ID,匹配成功则直接执行,无需 CAS 操作和线程阻塞
  3. 偏向锁的核心优化:消除无竞争场景下的加锁 / 解锁开销。
升级触发条件

第二个线程尝试竞争该锁时,偏向锁立即失效,升级为轻量级锁。

阶段 3:轻量级锁(Lightweight Lock)

适用场景

多线程交替执行同步代码块,短时间内无激烈的锁竞争(自旋即可获取锁)。

工作原理
  1. 竞争发生时,JVM 会在当前线程的栈帧中创建锁记录(Lock Record),将对象头的 Mark Word 拷贝到锁记录中;
  2. 线程通过CAS 操作尝试将对象头的 Mark Word 指向自己的锁记录,若成功则获取轻量级锁,执行同步代码;
  3. 若 CAS 失败,线程不会立即阻塞,而是执行自旋锁(循环尝试 CAS 获取锁),避免频繁切换内核态。
升级触发条件
  • 自旋次数达到阈值(JVM 自适应调整)仍未获取锁;
  • 出现第三个线程竞争锁(自旋会造成 CPU 空耗,触发升级)。

阶段 4:重量级锁(Heavyweight Lock)

适用场景

多线程高并发竞争,同步代码块执行时间较长,自旋会造成大量 CPU 资源浪费。

工作原理
  1. 锁升级为重量级锁后,JVM 会向操作系统申请互斥量(Mutex)
  2. 未获取锁的线程会被阻塞挂起,进入操作系统的等待队列,释放 CPU 资源;
  3. 持有锁的线程释放锁后,操作系统会唤醒等待队列中的线程,重新竞争锁。
特点

依赖操作系统内核实现,线程阻塞 / 唤醒会触发用户态↔内核态切换,性能开销最大,但能避免 CPU 空耗。

五、synchronized 与锁升级的核心关系

1. 从属与支撑关系

  • 锁升级是 synchronized 的性能优化方案:没有锁升级机制,synchronized 始终是低效的重量级锁;锁升级让 synchronized 适配了不同并发场景,成为高效的内置锁。
  • synchronized 是锁升级的应用载体:锁升级机制仅针对 Java 内置的 synchronized 锁生效,ReentrantLock 等显式锁不遵循该升级逻辑。

2. 动态适配关系

synchronized 的性能和行为完全由锁升级状态决定

锁状态 synchronized 性能 适用并发场景 核心开销
偏向锁 最高 单线程重复访问 几乎无开销
轻量级锁 较高 线程交替竞争、短时间同步 CAS 操作 + 自旋开销
重量级锁 最低 高并发激烈竞争 内核态切换 + 线程阻塞开销

3. 底层绑定关系

锁升级的状态切换,直接修改对象头 Mark Word的结构,而 synchronized 加锁的本质,就是操作对象头的锁标记和关联数据,二者通过对象头完成底层绑定。

六.synchronized的重量级锁底层如何实现的?

前文一直提到了一个很重要的东西,叫**对象头 Mark Word,**这里就带大家认识一下

Synchronized中的重量级锁,底层就是基于**锁监视器(Monitor)**来实现的。简单来说就是锁对象头会指向一个锁监视器,而在监视器中则会记录一些信息,比如:

  • _owner:持有锁的线程

  • _recursions:锁重入次数

因此每一个锁对象,都会指向一个锁监视器,而每一个锁监视器,同一时刻只能被一个线程持有 ,这样就实现了互斥效果。但前提是,多个线程使用的是同一把锁

前置核心概念
  1. 触发时机 :当轻量级锁的自旋获取锁失败、自旋次数超限,或出现多线程同时竞争时,JVM 会将锁升级为重量级锁
  2. 核心载体 :HotSpot 中,每个 Java 对象关联一个ObjectMonitor结构体(C++ 实现),这是重量级锁的核心数据结构;
  3. 底层依赖 :重量级锁会脱离纯用户态实现,调用操作系统内核的互斥锁、线程调度接口,触发用户态与内核态的切换。

核心数据结构:ObjectMonitor

重量级锁的所有逻辑都围绕ObjectMonitor展开,它存储了锁的持有状态、等待线程队列、竞争线程队列等关键信息,核心关键字段如下:

cpp 复制代码
// HotSpot源码简化版 ObjectMonitor 结构
class ObjectMonitor {
public:
    // 标识当前持有锁的线程,NULL表示锁未被占用
    void* _owner;
    // 锁重入计数(支持synchronized可重入特性)
    int _recursions;
    // 竞争锁失败,进入阻塞状态的线程队列(双向链表)
    ObjectWaiter* _EntryList;
    // 调用了wait()方法,处于等待唤醒的线程队列
    ObjectWaiter* _WaitSet;
    // 锁的状态标记
    int _count;
    // 底层操作系统互斥量(实现内核级阻塞的核心)
    pthread_mutex_t _mutex;
};

关键字段作用:

  • _owner:标记当前独占锁的线程,实现互斥访问;
  • _recursions:记录重入次数,退出同步块时递减,为 0 才真正释放锁;
  • _EntryList:存储尝试获取锁但失败的阻塞线程;
  • _WaitSet:存储执行了wait()、需要被notify()唤醒的线程。

重量级锁底层完整执行流程

整个流程分为加锁、锁重入、线程等待 / 唤醒、释放锁 四个核心阶段,全程由 JVM 底层操作ObjectMonitor和操作系统内核完成:

阶段 1:锁升级与初始化
  1. 轻量级锁竞争失败后,JVM 会为目标对象分配并关联 ObjectMonitor
  2. 将对象头Mark Word的锁标记修改为重量级锁状态 ,并存储指向ObjectMonitor的指针;
  3. 后续所有线程的加锁操作,都会通过这个监视器对象执行。
阶段 2:线程竞争加锁逻辑
  1. 线程尝试获取重量级锁时,首先通过操作系统互斥量_mutex 抢占锁权限;
  2. _ownerNULL(锁空闲):
    • 线程将_owner指向自身,_recursions置为 1,成功获取锁,进入同步代码块执行;
  3. _owner已指向其他线程(锁被占用):
    • 该线程被封装为ObjectWaiter节点,加入_EntryList阻塞队列;
    • JVM 调用操作系统内核接口,将线程挂起阻塞 ,释放 CPU 资源,线程状态变为BLOCKED
  4. 阻塞期间线程完全不消耗 CPU,直到被唤醒后重新竞争锁。
阶段 3:锁重入实现

synchronized是可重入锁,底层通过_recursions字段实现:

  • 持有锁的线程再次进入同一同步块 时,JVM 检测_owner为当前线程,直接将_recursions + 1,无需重新竞争;
  • 每退出一层同步块,_recursions - 1,直到计数归 0,才会真正释放锁。
阶段 4:wait () /notify () 唤醒逻辑

这是重量级锁独有的等待 / 唤醒机制,依赖_WaitSet队列:

  1. 持有锁的线程调用object.wait()
    • 释放当前持有的锁(重置_owner_recursions);
    • 线程从_EntryList移除,加入_WaitSet等待队列,进入WAITING状态;
  2. 其他线程调用object.notify()/notifyAll()
    • JVM 从_WaitSet中取出线程节点,转移回_EntryList
    • 线程被唤醒,重新参与锁竞争。
阶段 5:释放锁与线程唤醒
  1. 持有锁的线程执行完同步代码块,JVM 开始释放锁:
    • _recursions递减,若计数为 0,将_owner置为NULL,释放操作系统互斥量_mutex
  2. 监视器对象唤醒_EntryList中的阻塞线程(唤醒策略由 JVM 实现,如公平 / 非公平);
  3. 被唤醒的线程重新竞争锁,竞争成功则占用锁,失败则重新回到阻塞队列。

底层关键技术细节
1. 用户态与内核态切换

重量级锁最大的性能开销来源:

  • 线程阻塞、唤醒、互斥量操作,都需要调用操作系统内核 API,触发用户态→内核态→用户态的切换;
  • 切换过程涉及上下文保存 / 恢复、内核权限校验,开销远大于用户态的 CAS 操作。
2. 与轻量级锁的核心区别
特性 轻量级锁 重量级锁
实现层级 JVM 用户态纯代码实现 依赖操作系统内核实现
线程等待方式 自旋循环(消耗 CPU) 内核挂起阻塞(不消耗 CPU)
核心数据结构 线程栈帧中的锁记录 ObjectMonitor 监视器
性能开销 低(无状态切换) 高(存在内核态切换)
适用场景 短时间、低竞争 长时间、高竞争
3. 非公平特性

synchronized重量级锁默认是非公平锁:被唤醒的阻塞线程,与新创建的线程会同时竞争锁,不保证等待时间最长的线程优先获取锁,这能提升吞吐量,但可能造成线程饥饿。


重量级锁的优缺点
优点
  1. 高竞争下更高效:线程阻塞后释放 CPU,避免自旋导致的 CPU 空转浪费,适合同步代码执行耗时较长的场景;
  2. 功能完整 :支持wait()/notify()等待唤醒机制,支持锁重入、互斥访问,满足复杂并发场景;
  3. 稳定性强:依赖操作系统原生调度,不会因自旋耗尽系统资源。
缺点
  1. 性能开销大:线程阻塞 / 唤醒的内核态切换成本高,无竞争 / 低竞争场景下远不如偏向锁、轻量级锁;
  2. 灵活性低:无法手动控制超时、中断、公平性,仅支持 JVM 默认策略。

补充:与 ReentrantLock 底层的差异

很多人会对比synchronized重量级锁和ReentrantLock,二者核心区别:

  1. synchronized重量级锁:JVM 底层 C++ 实现,绑定 ObjectMonitor,依赖操作系统互斥量;
  2. ReentrantLockJava 代码层基于 AQS 实现,使用 CAS + 双向队列 + LockSupport,不依赖 ObjectMonitor,也没有内核态的强绑定。

总结

  1. 核心实现synchronized重量级锁以 HotSpot 的ObjectMonitor(C++ 结构体)为核心,依赖操作系统互斥量实现线程互斥;
  2. 执行逻辑 :锁被占用时,竞争线程进入_EntryList阻塞队列并被内核挂起,锁释放后唤醒线程重新竞争,同时通过_recursions支持可重入;
  3. 关键开销 :线程调度依赖用户态与内核态切换,这是重量级锁性能较低的核心原因;
  4. 适用场景 :专为高并发、长时间占用锁的场景设计,用线程阻塞换取 CPU 资源的合理利用。
相关推荐
!停2 小时前
数据结构二叉树——堆
java·数据结构·算法
£漫步 云端彡2 小时前
Golang学习历程【第十一篇 接口(interface)】
开发语言·学习·golang
Web项目开发2 小时前
Dockerfile创建Almalinux9镜像
linux·运维·服务器
virus594510 小时前
悟空CRM mybatis-3.5.3-mapper.dtd错误解决方案
java·开发语言·mybatis
pride.li10 小时前
开发板和Linux--nfs服务挂载
linux·运维·服务器
初次见面我叫泰隆10 小时前
Qt——3、常用控件
开发语言·qt·客户端
计算机毕设VX:Fegn089510 小时前
计算机毕业设计|基于springboot + vue蛋糕店管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
looking_for__10 小时前
【Linux】应用层协议
linux·服务器·网络
没差c11 小时前
springboot集成flyway
java·spring boot·后端