1. 从对象头到管程:Java监视器的底层架构
在HotSpot虚拟机中,每个Java对象都由三部分组成:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。其中对象头存储了与锁相关的关键信息,这正是synchronized实现的基础。对象头的结构会随着锁状态变化而动态调整:
code复制|-------------------------------------------------|
| Mark Word (64 bits) |
|-------------------------------------------------|
| Class Pointer (32/64 bits, depends) |
|-------------------------------------------------|
当对象未被锁定(普通状态)时,Mark Word的存储布局如下:
code复制|-------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |
|-------------------------------------------------|--------------------|
当对象被synchronized锁定(轻量级锁)时,Mark Word会被替换为指向线程栈中锁记录的指针:
code复制|-------------------------------------------------|
| ptr_to_lock_record:62 | lock:2 |
|-------------------------------------------------|
在重量级锁状态下,Mark Word则存储指向操作系统级互斥量(mutex)的指针:
code复制|-------------------------------------------------|
| ptr_to_heavyweight_monitor:62 | lock:2 |
|-------------------------------------------------|
这个重量级锁指针指向的就是真正的Monitor对象,也就是我们常说的"管程"。在HotSpot实现中,这个ObjectMonitor类包含以下关键字段:
cpp复制class ObjectMonitor {
volatile markOop _header; // 存储原始对象头
void* volatile _object; // 关联的Java对象
void* volatile _owner; // 持有锁的线程
volatile jlong _recursions; // 重入次数
ObjectWaiter* volatile _WaitSet; // 处于wait状态的线程链表
volatile int _WaitSetLock; // 保护WaitSet的锁
volatile int _count; // 剩余资源数
// ... 其他字段
};
当线程尝试获取锁时,会经历以下步骤:
- 通过CAS操作尝试将Mark Word替换为指向当前线程栈中锁记录的指针(轻量级锁)
- 如果失败且检测到锁膨胀(其他线程持有锁),则升级为重量级锁
- 最终通过操作系统的互斥量实现线程阻塞
关键细节:偏向锁(Biased Locking)是JVM的优化手段,它假设大多数情况下锁不存在竞争。当锁第一次被线程获取时,JVM会通过CAS操作将线程ID记录到Mark Word中,后续该线程可以直接进入临界区而无需同步操作。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. synchronized关键字的三重面孔
2.1 方法级同步的字节码实现
当synchronized修饰实例方法时,编译器会在方法访问标志中添加ACC_SYNCHRONIZED标记。方法调用时,执行引擎会检查这个标志:
java复制public synchronized void syncMethod() {
// 方法体
}
对应的字节码如下(通过javap -v查看):
code复制 public synchronized void syncMethod();
descriptor: ()V
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
JVM在方法调用和返回时会隐式插入monitorenter和monitorexit指令。这种实现方式比显式使用monitorenter/monitorexit更高效,因为减少了字节码数量。
2.2 代码块同步的指令分析
对于同步代码块,编译器会显式生成monitorenter和monitorexit指令:
java复制public void syncBlock() {
synchronized(this) {
// 代码块
}
}
对应的字节码显示为:
code复制 public void syncBlock();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter // 进入监视器
4: aload_1
5: monitorexit // 正常退出监视器
6: goto 14
9: astore_2
10: aload_1
11: monitorexit // 异常退出监视器
12: aload_2
13: athrow
14: return
值得注意的是,编译器会自动为同步代码块生成两个monitorexit指令:一个用于正常退出,另一个用于异常退出路径。这确保了即使在代码块抛出异常时,锁也能被正确释放。
2.3 静态方法同步的特殊性
静态方法的同步使用类的Class对象作为锁:
java复制public static synchronized void staticSync() {
// 静态方法体
}
这等价于:
java复制public static void staticSync() {
synchronized(MyClass.class) {
// 方法体
}
}
在类加载过程中,JVM会为每个类创建一个对应的Class对象。这个对象同样具有标准的对象头结构,因此静态同步与实例同步的底层机制完全相同,只是锁对象不同。
实践建议:避免使用静态同步方法锁住公共类(如Object.class或String.class),这可能导致意外的死锁。应该为每个功能域创建专用的私有锁对象。
3. 等待/通知机制的实现原理
3.1 wait()方法的完整执行路径
当线程调用wait()时,JVM会执行以下操作序列:
- 检查当前线程是否持有对象锁(否则抛出IllegalMonitorStateException)
- 将线程封装成ObjectWaiter节点加入_WaitSet队列
- 原子递减_count计数器(释放锁资源)
- 通过park()挂起线程,等待被唤醒
这个过程的伪代码如下:
java复制void ObjectMonitor::wait(jlong millis, bool interruptible) {
Thread * const Self = Thread::current();
// 检查锁持有状态
if (THREAD != _owner) {
throw IllegalMonitorStateException();
}
// 创建等待节点
ObjectWaiter node(Self);
node._notified = 0;
// 加入等待集
AddWaiter(&node);
// 释放锁
exit(true, Self);
// 挂起线程
while(node._notified == ) {
if (millis <= 0) {
Self->_ParkEvent->park();
} else {
Self->_ParkEvent->park(millis);
}
}
// 被唤醒后重新竞争锁
enter(Self);
}
3.2 notify()/notifyAll()的差异实现
notify()的实现相对简单:
java复制void ObjectMonitor::notify(Thread * Self) {
if (_WaitSet == NULL) return;
// 移出第一个等待者
ObjectWaiter * iterator = DequeueWaiter();
if (iterator != NULL) {
iterator->_notified = 1;
// 加入_EntryList等待锁
EnterEpilog(Self, iterator);
}
}
而notifyAll()需要遍历整个_WaitSet:
java复制void ObjectMonitor::notifyAll(Thread * Self) {
ObjectWaiter * iterator;
while ((iterator = DequeueWaiter()) != NULL) {
iterator->_notified = 1;
EnterEpilog(Self, iterator);
}
}
关键区别在于:
- notify()随机唤醒一个等待线程(实际是FIFO顺序)
- notifyAll()唤醒所有等待线程,它们需要重新竞争锁
- 被唤醒的线程不会立即执行,必须等到当前线程释放锁后
3.3 虚假唤醒(Spurious Wakeup)的应对
操作系统层面的线程调度可能导致wait()在没有notify()调用的情况下返回。Java官方推荐的标准处理模式是:
java复制synchronized(lock) {
while(!condition) { // 必须用while而不是if
lock.wait();
}
// 处理业务逻辑
}
这种模式能有效应对:
- 底层操作系统的虚假唤醒
- 其他线程意外调用了notify()
- 条件变量被提前修改
4. 从JVM到OS:锁的膨胀与收缩
4.1 偏向锁的获取与撤销
偏向锁的获取流程:
- 检查Mark Word的biased_lock标志位
- 如果可偏向(biased_lock=1且epoch有效):
- 检查线程ID是否指向当前线程
- 如果是,直接进入临界区
- 如果不是,尝试通过CAS替换线程ID
- 如果不可偏向,走轻量级锁流程
撤销偏向锁的典型场景:
- 多个线程交替访问同步块
- 调用hashCode()方法(因为Mark Word需要存储哈希码)
- 调用wait()/notify()方法(需要升级为重量级锁)
4.2 轻量级锁的自旋优化
轻量级锁通过线程栈中的Lock Record实现:
- 在当前线程栈帧中分配Lock Record空间
- 将对象头的Mark Word复制到Lock Record(Displaced Mark Word)
- 通过CAS将对象头替换为指向Lock Record的指针
- 如果成功,获取锁;如果失败,存在竞争
自旋锁优化:
cpp复制void ObjectMonitor::EnterI(TRAPS) {
if (TrySpin(Self) > 0) {
// 自旋成功获取锁
return;
}
// 自旋失败后进入重量级锁流程
EnterEpilog(Self);
}
自旋次数的自适应调整:
- 基于上次自旋是否成功动态调整
- 考虑CPU核心数(单核CPU通常禁用自旋)
- 通过-XX:PreBlockSpin参数设置初始值
4.3 重量级锁的底层支持
重量级锁最终依赖操作系统的互斥量实现:
- Linux下通过pthread_mutex_t实现
- Windows下通过CRITICAL_SECTION或SRWLock实现
ObjectMonitor的enter()方法核心逻辑:
cpp复制void ATTR ObjectMonitor::enter(TRAPS) {
Thread * const Self = THREAD;
// 快速路径尝试
if (TryLock(Self) > 0) return;
// 慢速路径
for (;;) {
if (TrySpin(Self) > 0) return;
if (_owner == NULL) {
if (CAS(&_owner, NULL, Self) == NULL) {
return;
}
}
// 最终进入操作系统级等待
Self->_ParkEvent->park();
}
}
5. 线程间通信的经典模式
5.1 生产者-消费者模型的正确实现
完整线程安全实现示例:
java复制class Buffer {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity;
public Buffer(int capacity) {
this.capacity = capacity;
}
public synchronized void produce(int item) throws InterruptedException {
while (queue.size() == capacity) {
wait(); // 缓冲区满时等待
}
queue.offer(item);
notifyAll(); // 通知可能存在的消费者
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // 缓冲区空时等待
}
int item = queue.poll();
notifyAll(); // 通知可能存在的生产者
return item;
}
}
关键改进点:
- 使用while循环检查条件,避免虚假唤醒
- 使用notifyAll()而非notify(),防止死锁
- 容量检查与操作原子化,防止竞态条件
5.2 读写锁的手动实现
基于wait/notify的简化实现:
java复制class ReadWriteLock {
private int readers = 0;
private int writers = 0;
private int writeRequests = 0;
public synchronized void lockRead() throws InterruptedException {
while (writers > 0 || writeRequests > 0) {
wait();
}
readers++;
}
public synchronized void unlockRead() {
readers--;
notifyAll();
}
public synchronized void lockWrite() throws InterruptedException {
writeRequests++;
while (readers > 0 || writers > 0) {
wait();
}
writeRequests--;
writers++;
}
public synchronized void unlockWrite() {
writers--;
notifyAll();
}
}
这种实现虽然简单,但存在写线程饥饿问题。Java官方ReentrantReadWriteLock使用了更复杂的队列机制保证公平性。
5.3 条件变量的高效使用
Java提供了更灵活的Condition接口:
java复制class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
Condition的优势:
- 可以创建多个等待条件
- 支持不响应中断的等待
- 提供精确唤醒(signal()而非signalAll())
- 通常与显式锁配合使用
6. 性能优化与问题排查
6.1 锁竞争的性能影响
高竞争环境下的性能表现:
- 轻量级锁在低竞争时性能接近无锁
- 重量级锁在高竞争时吞吐量急剧下降
- 自旋锁在短临界区效果良好
优化建议:
- 减小锁粒度(如ConcurrentHashMap的分段锁)
- 缩短持有锁的时间(不要在锁内执行IO操作)
- 考虑读写锁替代互斥锁
- 使用并发容器代替同步容器
6.2 死锁检测与分析
典型死锁场景:
java复制// 线程1
synchronized(lockA) {
synchronized(lockB) { ... }
}
// 线程2
synchronized(lockB) {
synchronized(lockA) { ... }
}
诊断工具:
- jstack:检测死锁并打印线程栈
code复制Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x00007f88e4003f58 (object 0x000000076abceb80, a java.lang.Object), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x00007f88e4003e98 (object 0x000000076abceb90, a java.lang.Object), which is held by "Thread-1" - JConsole/VisualVM:图形化显示线程状态
- 在线程转储中搜索"BLOCKED"状态
6.3 锁粗化与锁消除
JIT编译器的优化技术:
锁粗化(Lock Coarsening):
java复制// 优化前
for (int i = 0; i < 100; i++) {
synchronized(this) {
doSomething();
}
}
// 优化后
synchronized(this) {
for (int i = 0; i < 100; i++) {
doSomething();
}
}
锁消除(Lock Elision):
java复制public String concat(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
如果JVM能证明StringBuffer不会逃逸出方法,就会消除同步操作。
7. 现代Java并发工具的对比
7.1 ReentrantLock的进阶特性
相比synchronized的优势:
- 可中断的锁获取:lockInterruptibly()
- 定时锁等待:tryLock(long timeout, TimeUnit unit)
- 公平性选择:new ReentrantLock(true)
- 多条件变量:newCondition()
典型使用模式:
java复制Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
while (!conditionMet) {
condition.await();
}
// 处理业务
} finally {
lock.unlock();
}
7.2 StampedLock的乐观读
混合锁模式的典型实现:
java复制class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
7.3 虚拟线程与锁的关系
Java 19引入的虚拟线程(协程)仍然依赖监视器:
- 虚拟线程阻塞会挂起载体线程
- synchronized块仍然是绑定到操作系统线程的
- 推荐使用ReentrantLock替代synchronized
典型问题场景:
java复制synchronized(lock) {
Files.readAllBytes(path); // 会阻塞载体线程
}
改进方案:
java复制Lock lock = new ReentrantLock();
try {
lock.lock();
byte[] data = Files.readAllBytes(path);
} finally {
lock.unlock();
}
