目录
一、核心机制深度解析
1. 对象头(Object Header)与Mark Word的奥秘
2. Monitor:同步的实质
二、锁升级的全过程与底层操作
1. 无锁 -> 偏向锁
2. 偏向锁 -> 轻量级锁
3. 轻量级锁 -> 重量级锁
三、高级话题与实战调优
1. 锁的优劣对比
2. 锁降级
3. synchronized的性能问题
作为Java并发编程的基石,synchronized
关键字的重要性不言而喻。它不仅仅是一个关键字,更是一套完整的线程同步解决方案,其背后蕴含着Java虚拟机精湛的设计哲学。本文将带领你从字节码层面到操作系统内核,全方位剖析 synchronized
的实现原理、优化手段与实战技巧。
一、核心机制深度解析
synchronized
的实现建立在两个核心概念之上:对象头(Object Header) 和 Monitor(监视器)。理解这两者是掌握 synchronized
的关键。
1. 对象头(Object Header)与Mark Word的奥秘
每个Java对象在堆内存中的存储布局都包含以下部分:
-
对象头 (Object Header):包含两类信息:
-
Mark Word:这是实现锁的核心。在64位JVM中,它通常占64位(8字节),是一个动态变化的数据结构,其内容会根据锁的状态而发生改变。它用于存储对象的哈希码(hashCode)、GC分代年龄、锁状态标志、线程持有的锁信息、偏向线程ID等。
-
Klass Pointer:指向对象的类元数据(Class对象)的指针,JVM通过它来确定对象属于哪个类。在64位JVM中,默认开启指针压缩(-XX:+UseCompressedOops),此指针被压缩为32位。
-
-
实例数据 (Instance Data):对象真正存储的有效信息,即程序代码中定义的各种字段内容。
-
对齐填充 (Padding):起占位符作用,HotSpot VM要求对象起始地址必须是8字节的整数倍,因此需要对对象大小进行对齐填充。
Mark Word在不同锁状态下的结构是其精髓所在,如下图所示:
(此处应有一张Mark Word内存布局图,展示无锁、偏向锁、轻量级锁、重量级锁、GC标记状态下的位分布)
锁状态 | 偏向锁标志 (biased_lock) | 锁标志位 (lock) | 存储内容 |
---|---|---|---|
无锁 | 0 | 01 | 对象的哈希码(hashCode)、对象分代年龄 |
偏向锁 | 1 | 01 | 持有偏向锁的线程ID、epoch、对象分代年龄 |
轻量级锁 | - | 00 | 指向栈中锁记录(Lock Record)的指针 |
重量级锁 | - | 10 | 指向操作系统互斥量(Mutex)和等待队列的指针 |
GC标记 | - | 11 | 空(表示该对象正被垃圾回收) |
2. Monitor:同步的实质
每个Java对象都可以关联一个Monitor(监视器锁)。在HotSpot虚拟机中,Monitor是由 ObjectMonitor
类(C++实现)实现的,其主要数据结构包括:
-
_owner
:指向当前持有该Monitor的线程。 -
_EntryList
:存储所有阻塞等待获取该Monitor的线程。 -
_WaitSet
:存储调用了Object.wait()
方法而进入等待状态的线程。
当线程执行到 synchronized
保护的代码块时:
-
执行
monitorenter
指令,尝试通过CAS操作将Monitor的_owner
字段设置为当前线程。 -
如果成功,该线程即持有Monitor,进入临界区执行代码。
-
如果失败,说明Monitor已被其他线程持有,当前线程会进入
_EntryList
队列中阻塞等待。 -
持有Monitor的线程执行完同步代码后,会执行
monitorexit
指令,将_owner
置为null
并唤醒_EntryList
中的线程,它们将重新竞争锁。
二、锁升级的全过程与底层操作
锁升级是 synchronized
性能优化的核心,其目的是在无竞争或低竞争情况下,避免使用重量级锁带来的高昂开销。
1. 无锁 -> 偏向锁
-
触发条件:第一个线程访问同步块。
-
底层操作:JVM使用CAS操作,将当前线程ID写入对象头的Mark Word中,并设置偏向锁标志。如果成功,则线程成功获取偏向锁。
-
优势:此后该线程再进入同步块时,只需检查Mark Word中的线程ID是否为自己,是则直接执行,无需任何同步操作,开销极小。
2. 偏向锁 -> 轻量级锁
-
触发条件:有另一个线程来竞争锁(偏向锁发生撤销)。
-
底层操作:
-
首先,JVM会暂停拥有偏向锁的线程。
-
然后,在该线程的栈帧中创建一个锁记录(Lock Record) 空间。
-
将对象当前的Mark Word复制到该线程的锁记录中(称为Displaced Mark Word)。
-
线程使用CAS操作尝试将对象头的Mark Word替换为指向其锁记录的指针。
-
如果成功,当前线程获得轻量级锁;如果失败,表示存在竞争,进而升级为重量级锁。
-
-
优势:在没有真正竞争的情况下,使用CAS这种用户态操作避免了操作系统内核态切换的开销。
3. 轻量级锁 -> 重量级锁
-
触发条件:轻量级锁的CAS操作失败(自旋后仍无法获取锁)。
-
底层操作:
-
当前线程会先自旋一小段时间(自适应自旋),尝试获取锁,以避免直接升级带来的开销。
-
如果自旋后仍无法获取,锁正式升级为重量级锁。
-
JVM会向操作系统申请一个互斥量(Mutex),并将对象头的Mark Word更新为指向该互斥量的指针。
-
未能获取锁的线程会被挂起(park),进入
_EntryList
队列,等待操作系统的调度唤醒。
-
-
开销:线程的挂起和唤醒涉及用户态到内核态的切换,上下文切换成本非常高。
三、高级话题与实战调优
1. 锁的优劣对比
锁类型 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁解锁无额外开销 | 锁撤销有额外开销 | 单线程访问同步块 |
轻量级锁 | 竞争线程不阻塞,程序响应快 | 长时间自旋会消耗CPU | 追求响应时间,同步块执行快 |
重量级锁 | 竞争线程不自旋,不消耗CPU | 线程阻塞,响应慢 | 追求吞吐量,同步块执行时间长 |
2. 锁降级
锁降级确实存在,但其场景非常有限,主要发生在 GC的STW阶段。为了减少GC停顿时间,JVM会尝试进行锁降级。但在正常的用户代码执行路径中,锁升级是不可逆的,这是为了避免在重量级锁和轻量级锁之间反复切换带来的巨大性能损耗。
3. 性能调优最佳实践
-
减少锁粒度:缩小同步代码块的范围,最经典的例子是
ConcurrentHashMap
使用分段锁。 -
减少锁持有时间:避免在同步块内执行耗时的I/O操作、网络请求或复杂计算。
-
读写分离:使用
ReadWriteLock
替代独占锁,允许读读并发,提高读多写少场景的性能。 -
JVM参数调优:
-
-XX:-UseBiasedLocking
:明确知道会有高竞争时,可禁用偏向锁。 -
-XX:BiasedLockingStartupDelay=0
:取消偏向锁延迟(默认4s),适用于启动后立即高并发的应用。 -
-XX:+UseSpinning
/-XX:PreBlockSpin
:控制轻量级锁的自旋策略(JDK6之后是自适应自旋,通常无需手动调整)。
-
3. synchronized的性能问题
- 锁粒度太大:同步范围覆盖过多无关代码,导致线程竞争加剧。
- 锁持有时间过长:同步块中包含耗时操作。
- 锁竞争激烈:多个线程频繁争抢同一把锁,导致上下文切换频繁。
- 锁升级频繁:大量竞争导致锁从偏向锁升级到重量级锁。
- 死锁:线程互相等待对方释放锁,导致系统停滞。