这是一个经常被问道的问题。首先记住一个分界线。Java 6 之前基于重量级锁实现,之后引入锁升级(偏向锁->轻量级锁->重量级锁)减少频繁切换上下文的开销。
Java 6 之前早期版本的Synchronized 是基于重量级锁实现的。当一个线程访问一个被Synchronized 修饰的方法或者代码块,它会获取对象的锁,这个锁是与对象相关联的,每个对象都有一个锁标记。如果另外一个对象也想要访问这个方法或者代码块,它会首先检查对象是不是已经被锁定。如果已经被锁定,这个线程就会进入阻塞状态,等待持有锁的线程释放锁。这种阻塞和唤醒线程的操作涉及到操作系统层面上的上下文的切换,开销比较大,所以称之为重量级锁。
Java 6 之后引入了偏向锁,轻量级锁概念。底层实现依赖于 JVM 的监视器锁(Monitor)机制,主要涉及对象头、Monitor 对象以及字节码指令。以下是其核心原理的详细分析:
对象头与 Mark Word
每个 Java 对象在堆内存中存储时,其对象头(Object Header)包含两部分信息:
- Mark Word:存储对象的哈希码、锁状态标志等。
- Klass Pointer:指向对象所属类的元数据。
在加锁过程中,Mark Word 是关键。根据锁状态的不同,Mark Word 会动态变化:
- 无锁状态:存储对象的哈希码、分代年龄等。
- 偏向锁:存储持有偏向锁的线程 ID。
- 轻量级锁:存储指向栈中锁记录的指针。
- 重量级锁:存储指向 Monitor 对象的指针。
Monitor 机制
Monitor 是 JVM 实现的同步机制,每个对象关联一个 Monitor。其核心结构包括:
- Owner:记录持有锁的线程。
- EntryList:存放阻塞等待锁的线程。
- WaitSet:存放调用
wait()后进入等待状态的线程。
当一个线程尝试获取锁时:
- 如果锁未被占用,线程成为 Owner。
- 如果锁被占用,线程进入 EntryList 阻塞等待。
- 当 Owner 释放锁时,JVM 会从 EntryList 唤醒一个线程竞争锁。
锁升级过程
JVM 对 Synchronized 的优化包括锁升级,分为以下阶段:
偏向锁
- 场景:只有一个线程访问同步块。
- 原理:在 Mark Word 中记录线程 ID,避免 CAS 操作。
- 触发条件:通过
-XX:+UseBiasedLocking开启(JDK 15 后默认禁用)。
轻量级锁
- 场景:多线程交替执行,无竞争。
- 原理:通过 CAS 将 Mark Word 替换为线程栈中的锁记录指针。
- 失败处理:若 CAS 失败,说明存在竞争,升级为重量级锁。
重量级锁
- 场景:多线程竞争激烈。
- 原理:通过操作系统互斥量(Mutex)实现,线程阻塞并进入内核态。
字节码层面实现
通过 javap -v 反编译同步代码块,可以看到以下指令:
- monitorenter:尝试获取锁。
- monitorexit:释放锁(确保在异常时也能释放,编译器会生成隐式 try-finally)。
示例字节码:
public void syncMethod() {
synchronized(this) {
System.out.println("Hello");
}
}
对应字节码:
aload_0 // 加载 this 引用
dup // 复制引用
astore_1 // 存储到局部变量表
monitorenter // 加锁
... // 同步块代码
aload_1 // 加载锁引用
monitorexit // 释放锁
性能优化建议
- 减少同步块范围,避免锁粗化。
- 对于高并发场景,优先考虑
java.util.concurrent包中的工具类(如ReentrantLock)。 - 避免在循环内加锁,防止不必要的锁竞争。
通过理解 Synchronized 的底层机制,可以更高效地设计线程安全程序。
636

被折叠的 条评论
为什么被折叠?



