synchronized 底层原理

synchonized 的底层原理是 Java 对象头Monitor(监视器) 共同作用的结果。它的实现过程会随着锁的竞争情况而升级,这个过程就是著名的 锁升级


核心概览

synchonized 关键字在 JVM 中的实现原理,可以概括为以下几个关键点:

  1. 基本单位:它锁住的是对象,而不是代码
    • 普通同步方法,锁住当前实例对象。
    • 静态同步方法,锁住当前类的Class对象。
    • 同步代码块,锁住synchronized括号里配置的对象。
  2. 实现基础:依赖于 Java 对象头 中的 Mark Word 来存储锁状态信息。
  3. 核心机制:通过与一个与对象关联的 Monitor 进行交互来实现线程的互斥。
  4. 优化策略:为了在性能和开销之间取得平衡,JVM 引入了 锁升级 机制,使得 synchronized 的性能在现代 JDK 中已经大幅提升。

1. Java 对象头

在 JVM 中,每个 Java 对象在内存中分为三部分:对象头、实例数据和对齐填充字节。其中,对象头是理解锁的关键。对象头主要包含两类信息:

  • Mark Word:用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁标志等。这是实现锁的核心部分。
  • Klass Pointer:指向对象元数据的指针,JVM 通过它来确定这个对象是哪个类的实例。

为了用尽可能少的空间存储更多的信息,Mark Word 被设计成一个非固定的动态数据结构。它在不同锁状态下的存储内容是不同的。下图清晰地展示了其结构:

32位JVM的Mark Word结构

锁状态

无锁状态
25bit: 哈希码
4bit: 分代年龄
1bit: 偏向模式0
2bit: 锁标志 01

偏向锁状态
23bit: 线程ID
2bit: Epoch
4bit: 分代年龄
1bit: 偏向模式1
2bit: 锁标志 01

轻量级锁状态
30bit: 指向栈中锁记录的指针
2bit: 锁标志 00

重量级锁状态
30bit: 指向互斥量(Monitor)的指针
2bit: 锁标志 10

64位JVM的Mark Word结构

锁状态

无锁状态
未使用: 25bit
哈希码: 31bit
未使用: 1bit
分代年龄: 4bit
偏向模式: 1bit
锁标志: 2bit 01

偏向锁状态
线程ID: 54bit
Epoch: 2bit
未使用: 1bit
分代年龄: 4bit
偏向模式: 1bit
锁标志: 2bit 01

轻量级锁状态
指向栈中锁记录的指针: 62bit
锁标志: 2bit 00

重量级锁状态
指向互斥量(Monitor)的指针: 62bit
锁标志: 2bit 10


2. Monitor(监视器)

Monitor 是线程私有的数据结构,每一个被锁住的对象都会和一个 Monitor 关联。可以把它理解为一个特殊的房间,这个房间有一些规则:

  • Owner:当前持有锁的线程。
  • EntryList:等待锁的阻塞队列。多个线程竞争锁时,没有抢到的线程会进入这个队列等待。
  • WaitSet:等待队列。那些获得了锁的线程,如果调用了 wait() 方法,就会释放锁并进入这个集合,等待被 notify() 唤醒。

synchronized 的互斥性就是通过 Monitor 实现的

  1. 当线程执行到 synchronized 修饰的代码块时,JVM 会尝试将对象的 Mark Word 指向一个 Monitor,并将 Owner 设置为当前线程。
  2. 如果设置成功,该线程就成功获取了锁,可以执行同步代码。
  3. 此时如果另一个线程也来执行同步代码,它会发现 Owner 已经是别的线程,于是这个新线程会进入 EntryList 阻塞等待。
  4. 当持有锁的线程执行完同步代码块后,会释放锁(将 Owner 置为空),并唤醒 EntryList 中等待的线程来重新竞争锁。

3. 锁升级:性能优化的关键

早期的 synchronized 是纯粹的“重量级锁”,直接和操作系统底层的互斥量(Mutex Lock)关联,导致线程的挂起和唤醒需要切换到内核态,开销非常大。

为了优化性能,JDK 1.6 引入了锁升级机制,其路径是:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。这个升级过程是单向的,目的是减少获得锁和释放锁带来的性能消耗。

第 1 级:偏向锁
  • 场景:假设在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。
  • 原理:当一个线程第一次获得锁时,JVM 会将锁标志位设为“偏向模式”(01),并将线程ID记录到 Mark Word 中。以后这个线程再次进入同步代码块时,只需要检查 Mark Word 中的线程ID是否是自己。如果是,则无需任何同步操作(如CAS),直接进入,开销极小。
  • 目的:消除数据在无竞争情况下的同步开销,提高单线程执行同步代码块的性能。
  • 升级:一旦有另一个线程来尝试获取锁,偏向锁模式就会宣告结束,开始升级为轻量级锁。
第 2 级:轻量级锁
  • 场景:当锁是偏向锁,但有另一个线程来竞争时(轻度竞争)。
  • 原理
    1. 加锁:JVM 会在当前线程的栈帧中创建一个名为“锁记录”的空间,然后将对象的 Mark Word 复制到其中。然后,JVM 使用 CAS 操作 尝试将对象的 Mark Word 更新为指向该锁记录的指针。
    2. 如果成功,当前线程获得锁,锁标志位变为 00(轻量级锁状态)。
    3. 如果失败(说明有其他线程也在竞争),当前线程会通过 自旋 的方式不断尝试获取锁(空循环,不立即放弃CPU)。
  • 目的:避免线程在轻度竞争下直接进入重量级锁的阻塞状态,减少用户态到内核态的切换开销。
  • 升级:如果自旋了一定次数后(JDK 1.6 后是自适应的)还没有获得锁,或者有第三个线程来竞争,轻量级锁就会升级为重量级锁。
第 3 级:重量级锁
  • 场景:线程竞争非常激烈,轻量级锁的自旋消耗了大量CPU资源。
  • 原理:此时锁的标志位变为 10,Mark Word 中存储的是指向重量级锁(Monitor)的指针。所有等待锁的线程都会进入 EntryList 并被阻塞,需要操作系统的介入进行线程的调度和唤醒。
  • 特点:开销最大,但可以避免CPU的空转。

字节码层面

从字节码角度看,synchronized 同步语句块的实现是基于 monitorentermonitorexit 指令。

  • 在代码块开始处插入 monitorenter 指令,用于尝试获取锁。
  • 在代码块结束处和异常处插入 monitorexit 指令,用于释放锁。

JVM 要保证每个 monitorenter 必须有对应的 monitorexit

总结

特性描述
锁住对象锁是关联在对象上的,通过对象头的 Mark Word 来标识。
Monitor是实现互斥的核心机制,负责管理线程的排队和阻塞。
锁升级核心优化策略,路径为:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁,根据竞争激烈程度动态调整,兼顾了无竞争和高竞争场景下的性能。
现代性能在低竞争场景下,synchronized 的性能与 ReentrantLock 相差无几,因其是 JVM 内置特性,推荐优先使用。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值