深入了解synchronized
1、synchronized
底层实现原理
synchronized
是 Java 的关键字,但它的功能并不是由 Java 代码直接实现的,而是由 JVM 底层(主要是对象监视器机制) 提供支持;核心概念包括:
1、Monitor(监视器 / 管程)
在 Java 中,每个对象都有一个与之关联的 Monitor(监视器锁),它用于控制多个线程对临界资源的访问,是实现 synchronized
的基础。
- 每个 Java 对象都可以作为一个 锁(Lock),即 Monitor 对象。
- 当线程进入
synchronized
方法/代码块时,它必须先 获取该对象的 Monitor 锁; - 如果锁被其他线程占用,则当前线程进入 阻塞状态(BLOCKED),等待锁释放;
- 当线程退出
synchronized
块/方法时,会 自动释放 Monitor 锁。
2、对象头与 Mark Word(与锁状态相关)
在 JVM 中,每个对象在内存中的布局分为三部分:
1、对象头(Object Header)
2、实例数据(Instance Data)
3、对齐填充(Padding)
其中,对象头包含 Mark Word(标记字段),它存储了对象的 哈希码、GC 分代年龄、锁状态标志等信息。
🎯 锁的信息是记录在对象头的 Mark Word 中的!这是 synchronized 实现锁机制的关键。
3、Monitor 的底层实现(C++层面,HotSpot 虚拟机)
在 HotSpot 虚拟机中,每个 Java 对象都与一个 Monitor 对象关联,这个 Monitor 是用 C++ 实现的,核心结构是 ObjectMonitor
,它包含如下关键信息:
_owner
:指向持有该 Monitor 锁的线程_EntryList
:等待获取锁的线程队列(阻塞态)_WaitSet
:调用了wait()
方法的线程集合
当线程进入 synchronized
块时:
- 尝试通过 CAS(Compare And Swap)获取对象的 Monitor;
- 如果获取失败,线程进入阻塞状态,进入
_EntryList
; - 当锁被释放时,JVM 会唤醒
_EntryList
中的某个线程来竞争锁。
2、synchronized 的锁升级机制(锁优化)
在 Java 6 以后,JVM 对 synchronized
进行了大量的性能优化,引入了 锁升级(Lock Escalation)机制,即:根据竞争情况,锁的状态会从轻量级逐步升级到重量级,以提升性能。
🔒 锁的四种状态(由低到高,优化路径):
锁状态 | 说明 | 适用场景 | 实现方式 |
---|---|---|---|
无锁状态 | 对象刚创建,还没有线程竞争 | 初始状态 | - |
偏向锁(Biased Lock) | 假设锁通常由同一个线程获得,避免频繁的同步操作 | 单线程访问同步块 | 对象头记录偏向线程ID,无需 CAS |
轻量级锁(Lightweight Lock) | 当有少量线程交替访问,没有真正竞争,采用 CAS 自旋方式获取锁 | 多线程交替但低竞争 | 基于 CAS 和栈帧中的 Lock Record |
重量级锁(Heavyweight Lock) | 当多个线程真正竞争同一个锁,线程会进入阻塞状态,由操作系统介入管理 | 高并发、激烈竞争 | 基于 Monitor,线程进入 WAITING 状态 |
锁升级简化流程:
1、无锁 → 偏向锁:如果检测到总是同一个线程获取锁,JVM 会启用偏向锁,避免同步开销。
2、偏向锁 → 轻量级锁:当有第二个线程尝试获取锁时,偏向锁会撤销,升级为轻量级锁。
3、轻量级锁 → 重量级锁:如果自旋等待超过阈值(或竞争激烈),轻量级锁会升级为重量级锁,线程进入阻塞。
✅ 目的:避免所有同步都直接使用重量级锁(性能差),通过锁升级,在无竞争或低竞争时使用更轻量的机制。
3、synchronized 与 ReentrantLock 对比
对比维度 | synchronized | ReentrantLock |
---|---|---|
锁类型 | JVM 内置关键字,隐式锁 | JDK 提供的类,显式锁(需要手动加锁/释放) |
使用方式 | 直接加在方法或代码块上 | 需要使用 lock() 和 unlock() 方法 |
锁获取与释放 | 自动管理(进入/退出时) | 必须手动调用 lock() 和 unlock() ,通常配合 try-finally |
可中断性 | ❌ 不支持中断等待锁 | ✅ 支持 lockInterruptibly() |
超时机制 | ❌ 不支持尝试获取锁超时 | ✅ 支持 tryLock(long timeout, TimeUnit unit) |
公平性 | ❌ 默认非公平锁 | ✅ 支持公平锁和非公平锁(构造方法指定) |
条件变量 | 只能配合 wait() / notify() | ✅ 支持多个 Condition ,更灵活的线程协作 |
性能(Java 6+) | 优化良好,大多数场景足够 | 高竞争下可能表现更好,但使用复杂 |
总结:
- 如果使用场景 不复杂,追求代码简洁,使用 synchronized 就足够了;
- 如果需要 更灵活的锁控制(如超时、可中断、公平性、多个条件变量),选择 ReentrantLock。
4、synchronized 与 volatile 对比
对比维度 | synchronized | volatile |
---|---|---|
作用 | 保证原子性、可见性、有序性(互斥访问) | 仅保证 可见性和有序性,不保证原子性 |
线程安全 | 能保证复合操作的线程安全(如 i++) | 不能保证复合操作(如 i++ 是读改写三步操作) |
阻塞机制 | 获取不到锁的线程会 阻塞 | 不涉及阻塞,线程始终在运行 |
使用场景 | 用于方法或代码块,控制多线程对共享资源的访问 | 用于修饰变量,保证变量的可见性 |
性能 | 相对较重(尤其在竞争激烈时) | 更轻量,适合一写多读的场景 |
底层实现 | 基于 Monitor 锁 | 基于内存屏障(Memory Barrier) |
总结:
volatile
适用于 一个线程写、多个线程读的变量同步;- 但 不能保证复合操作(如 i++、check-then-act)的原子性,不能替代 synchronized。
5、synchronized、ReentrantLock、volatile总结
机制 | 作用 | 是否阻塞 | 是否可重入 | 是否公平 | 适用场景 |
---|---|---|---|---|---|
synchronized | 方法/代码块同步,保证原子性、可见性、有序性 | 是(自动) | ✅ 是 | ❌ 非公平(默认) | 简单线程同步,代码简洁优先 |
ReentrantLock | 显式锁,更灵活的线程同步控制 | 是(手动) | ✅ 是 | ✅ 可配置公平/非公平 | 需要高级功能:超时、中断、多条件 |
volatile | 保证变量的可见性与有序性,不保证原子性 | 否 | 不适用 | 不适用 | 单写多读、状态标志等轻量同步 |