Java 中的 synchronized
关键字用于实现线程之间的同步,保证共享资源在多线程环境下被安全地访问。其底层原理涉及多个步骤和机制,依赖于 JVM 内置的锁机制和对象头结构。这里是它的底层工作原理及锁的相关细节:
1. Java 对象头(Object Header)和 Mark Word
每个 Java 对象都包含一个对象头(Object Header),其中存储了锁的信息。Java 对象头包含两个主要部分:
- Mark Word:存储了锁状态、GC 标记、哈希码等信息。Mark Word 的大小为 32 位或 64 位,取决于 JVM 的位数。
- Class Pointer:指向对象的类元数据。
在未加锁状态下,Mark Word 包含对象的哈希值和 GC 标记信息。当一个线程对该对象加锁时,Mark Word 被修改以保存线程持有锁的信息。
2. 锁的状态
Mark Word
中记录了锁的状态,锁的状态在运行时是可以升级的,主要有以下几种:
- 偏向锁(Biased Locking):线程第一次加锁时将锁偏向该线程,如果再次加锁且没有其他线程争用时,锁依然保持偏向状态,无需做额外操作,开销极小。偏向锁使用了
Mark Word
中线程 ID 来记录持有锁的线程。 - 轻量级锁(Lightweight Locking):如果有多个线程争用,锁会升级为轻量级锁。轻量级锁使用 CAS(Compare-And-Swap)操作来尝试获取锁,通过自旋等待方式避免线程阻塞。
- 重量级锁(Heavyweight Locking):当争用激烈且轻量级锁自旋失败时,锁会升级为重量级锁,此时线程会进入阻塞状态,重量级锁使用操作系统的互斥量(Mutex)机制。
3. 锁的获取与释放流程
- 加锁(锁的获取):
- 无锁状态:当线程首次尝试获取锁时,JVM 会将对象头的
Mark Word
设置为偏向锁的状态,并将持有锁的线程 ID 写入 Mark Word 中。 - 轻量级锁:如果偏向锁失败(有多个线程竞争锁),锁会升级为轻量级锁。JVM 为当前线程创建一个栈帧中的锁记录,将对象头的
Mark Word
拷贝到锁记录中,然后尝试用 CAS 操作将对象头指向锁记录。 - 重量级锁:当轻量级锁的 CAS 自旋操作多次失败时,锁会升级为重量级锁。此时,线程会被阻塞,直到锁被释放。
- 无锁状态:当线程首次尝试获取锁时,JVM 会将对象头的
- 解锁(锁的释放):
- 线程持有锁时,当退出同步代码块,JVM 会根据锁的当前状态,恢复对象头的
Mark Word
。偏向锁和轻量级锁时,会将锁信息恢复到解锁前的状态。 - 如果锁升级为重量级锁,解锁时会唤醒被阻塞的线程。
- 线程持有锁时,当退出同步代码块,JVM 会根据锁的当前状态,恢复对象头的
4. 锁的状态标志(Mark Word 内容)
Java 中 Mark Word
的锁状态可以通过其中的几个位来标志不同状态:
- 无锁状态:标志位为 01,表示对象头未被锁。
- 偏向锁:标志位为 101,表示锁偏向某个线程。
- 轻量级锁:标志位为 00,表示处于轻量级锁状态。
- 重量级锁:标志位为 10,表示处于重量级锁状态。
5. 线程如何知道对象被锁住了?
当一个线程尝试获取锁时,会检查对象头中的 Mark Word
。如果发现对象头中的标志位已经被修改(例如,存在另一个线程持有偏向锁或轻量级锁),线程会通过 CAS 操作竞争锁,并根据不同的锁状态进行升级或阻塞。
6. 指令层面:MonitorEnter 与 MonitorExit
synchronized
关键字在字节码层面会转换为两条指令:
MonitorEnter
:表示线程试图进入同步块并获取锁。MonitorExit
:表示线程释放锁。
这两条指令与 JVM 内部的对象头和 Mark Word
状态紧密相关,JVM 会使用这些指令来进行加锁和解锁操作。
总结:
- 锁的获取过程:
synchronized
使用对象的Mark Word
来记录锁的状态,通过偏向锁、轻量级锁和重量级锁的升级来优化性能。 - 锁的同步机制:线程通过
MonitorEnter
和MonitorExit
指令实现同步操作,并通过 CAS 自旋、锁状态标志以及系统调用等机制来协调多线程访问共享资源。
这种分级锁策略极大地提高了 Java 的并发性能,减少了线程竞争时的上下文切换开销。