在多线程编程中,synchronized
是 Java 中最基础也最重要的同步机制之一。虽然它在 JDK 早期版本中因性能问题被诟病,但随着 JDK 1.6 引入的锁优化技术,它已经成为兼具性能和易用性的同步方案。本文将深入剖析 synchronized 的底层原理、锁升级过程以及 JVM 对它的各种优化措施。
一、synchronized 的三种使用形式
在深入原理前,先回顾一下 synchronized 的三种基本使用形式:
- 修饰实例方法:锁定当前对象实例
- 修饰静态方法:锁定当前类的 Class 对象
- 修饰代码块:锁定指定的对象
public class SynchronizedDemo {
// 1. 修饰实例方法(锁是当前实例对象)
public synchronized void instanceMethod() {
// 临界区代码
System.out.println("实例方法同步");
}
// 2. 修饰静态方法(锁是当前类的Class对象)
public static synchronized void staticMethod() {
// 临界区代码
System.out.println("静态方法同步");
}
// 3. 修饰代码块(锁是括号里指定的对象)
public void blockMethod() {
synchronized(this) {
// 临界区代码
System.out.println("代码块同步(this)");
}
synchronized(SynchronizedDemo.class) {
// 临界区代码
System.out.println("代码块同步(类对象)");
}
Object lock = new Object();
synchronized(lock) {
// 临界区代码
System.out.println("代码块同步(任意对象)");
}
}
}
二、synchronized 的底层实现原理
要真正理解 synchronized,必须从 JVM 层面看它是如何实现的。
1. 字节码层面
当 synchronized 修饰代码块时,JVM 会在同步块的前后分别插入monitorenter
和monitorexit
指令:
public void syncBlock() {
synchronized(this) {
System.out.println("同步块");
}
}
使用javap -c
命令反编译上述方法,可以看到:
public void syncBlock();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter // 进入同步块
4: getstatic #2 // 同步块内代码
...
13: aload_1
14: monitorexit // 退出同步块
...
当 synchronized 修饰方法时,JVM 不会使用 monitorenter 和 monitorexit 指令,而是在方法的访问标志中增加 ACC_SYNCHRONIZED 标志:
method_info {
u2 access_flags; // 访问标志,其中包含ACC_SYNCHRONIZED
u2 name_index;
u2 descriptor_index;
...
}
2. Monitor 机制
无论哪种形式,synchronized 的底层都依赖 Monitor(监视器)机制实现。每个对象都有一个关联的 Monitor:
Mark Word 记录了对象的状态,包括锁信息、垃圾回收信息、hashCode 等。
3. Monitor 核心数据结构
Monitor 本质上是一个同步工具:
- Owner:持有锁的线程
- Entry Set:等待获取锁的线程集合
- Wait Set:调用 wait()方法后,线程进入此队列
- 计数器:记录重入次数,实现 synchronized 的可重入性,当同一线程多次获取同一锁时,计数器累加,释放锁时递减,类似于 ReentrantLock 中的 holdCount
Monitor 对象由 JVM 在堆中创建,重量级锁状态下,对象头的 Mark Word 中存储的是指向该 Monitor 对象的指针。
三、锁的升级过程
JDK 1.6 引入了锁优化,核心是锁的升级过程:偏向锁 → 轻量级锁 → 重量级锁。
1. 偏向锁
偏向锁的核心思想:大多数情况下,锁不存在竞争,同一个线程反复获取同一把锁。
// 偏向锁示例
public class BiasedLockDemo {
public static void main(String[] args) throws Exception {
// JVM默认启用偏向锁,但有4秒延迟
// 可以通过-XX:BiasedLockingStartupDelay=0取消延迟
// 等待偏向锁机制激活
Thread.sleep(5000);
Object lock = new Object();
// 同一个线程多次获取锁
for (int i = 0; i < 5; i++) {
synchronized (lock) {
// 临界区代码
System.out.println("偏向锁生效中...");
}
}
}
}
偏向锁在 Mark Word 中记录线程 ID 和 epoch 值,下次相同线程获取锁时,通过比对线程 ID 直接获取锁,无需 CAS 操作。Mark Word 中的标志位为01
,且偏向标记为1
。
偏向锁撤销的触发条件包括:
- 其他线程竞争该锁
- 调用对象的 hashCode()方法(偏向锁没有存储 hashCode 的空间)
- GC 过程中发现有偏向锁
- 显式禁用偏向锁(
-XX:-UseBiasedLocking
)
2. 轻量级锁
当有第二个线程尝试获取锁时,偏向锁升级为轻量级锁。轻量级锁通过 CAS(Compare and Swap)操作替代重量级锁的互斥量操作。
轻量级锁的核心是:
- 线程在自己的栈帧中创建 Displaced Mark Word,存储对象头 Mark Word 的备份
- 通过 CAS 操作,将对象头中的 Mark Word 替换为指向线程栈中锁记录的指针
- 若 CAS 成功,获取轻量级锁成功;若失败,进入自旋等待或升级为重量级锁
此时 Mark Word 中的标志位为00
。
// 轻量级锁示例
public class LightweightLockDemo {
public static void main(String[] args) {
final Object lock = new Object();
// 创建两个线程竞争锁
new Thread(() -> {
synchronized (lock) {
System.out.println("Thread 1 acquired lock");
try {
Thread.sleep(20); // 保持锁一段时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 稍等片刻再启动第二个线程
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
synchronized (lock) {
System.out.println("Thread 2 acquired lock");
}
}).start();
}
}
3. 重量级锁
当以下情况发生时,轻量级锁会升级为重量级锁:
- 自旋达到阈值(默认 10 次,可通过
-XX:PreBlockSpin
设置) - 有多个线程同时竞争锁,自旋不再是有效的等待方式
- 持有锁的线程在自旋期间未释放锁(如执行时间较长的临界区)
重量级锁使用操作系统的互斥量(mutex)实现。进入重量级锁状态后,没有获得锁的线程会被阻塞,直到持有锁的线程释放锁。此时 Mark Word 中的标志位为10
,存储的是指向 Monitor 对象的指针。
重量级锁状态下,Mark Word 中存储的是指向堆中 Monitor 对象的指针。这个 Monitor 对象包含了 Owner、Entry Set、Wait Set 等结构,用于管理线程的阻塞和唤醒。
// 重量级锁示例(多线程高竞争)
public class HeavyweightLockDemo {
private static final int THREAD_COUNT = 20;
private static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
synchronized (HeavyweightLockDemo.class) {
counter++;
}
}
});
threads[i].start();
}
// 等待所有线程执行完成
for (Thread thread : threads) {
thread.join();
}
System.out.println("最终计数: " + counter);
}
}
4. 锁状态转换图
四、JVM 对 synchronized 的优化
1. 锁消除
JIT 编译器在运行时通过逃逸分析,检测到某些同步代码不可能存在竞争(对象仅在方法内部使用,未逃逸到其他线程),会自动消除锁操作。
public class LockEliminationDemo {
public void method() {
// StringBuffer是线程安全的,内部使用synchronized
StringBuffer sb = new StringBuffer();
sb.append("hello");
sb.append("world");
// JIT编译器通过逃逸分析发现sb仅在方法内部使用,不存在竞争,可以消除锁
}
}
锁消除优化默认启用,可以通过参数-XX:+DoEscapeAnalysis -XX:+EliminateLocks
控制(逃逸分析是锁消除的前提)。只有在确认对象不会"逃逸"到当前线程之外被其他线程访问时,JVM 才会消除锁。
2. 锁粗化
JVM 检测到连续对同一锁的请求与释放操作,会将多个连续的锁操作合并为一个更大范围的锁。这减少了加锁解锁的频繁操作,提高性能。
锁粗化中的"连续"指的是无其他代码插入的紧接同步块:
public class LockCoarseningDemo {
public void method() {
// 锁粗化可能生效(中间无其他操作)
synchronized(this) { System.out.println("操作1"); }
synchronized(this) { System.out.println("操作2"); }
synchronized(this) { System.out.println("操作3"); }
// JVM优化后可能变为:
/*
synchronized(this) {
System.out.println("操作1");
System.out.println("操作2");
System.out.println("操作3");
}
*/
// 而下面的代码,锁粗化可能不生效(中间有其他逻辑)
synchronized(this) { System.out.println("操作A"); }
System.out.println("非同步代码");
synchronized(this) { System.out.println("操作B"); }
}
}
3. 自适应自旋
轻量级锁自旋等待时,JVM 会根据上一次自旋等待的成功与否以及锁的持有时间动态调整自旋的次数。如果前一次自旋成功获得过锁,那么下一次自旋的次数可能会更多;如果自旋很少成功获得锁,那么会减少自旋次数或者直接升级为重量级锁。
自旋通过忙等待(Busy Waiting)消耗 CPU 资源,适用于临界区执行时间非常短的场景。在这种情况下,线程阻塞/唤醒的开销可能远大于自旋等待的开销。但如果临界区执行时间较长,持有锁的线程不会很快释放锁,此时自旋会白白消耗 CPU 资源,反而导致性能下降,这种情况下升级为重量级锁更为合适。
自适应自旋默认启用,可通过-XX:-UseSpinning
禁用(在新版 JDK 中被移除,默认总是启用自旋)。
五、类锁与对象锁的区别
类锁与对象锁的本质区别在于锁定的对象不同:
- 类锁:锁的是类的 Class 对象,全局唯一
- 对象锁:锁的是实例对象,每个实例都有独立的锁
public class LockTypeDemo {
// 对象锁:修饰实例方法
public synchronized void instanceMethod() {
System.out.println(Thread.currentThread().getName() + " 获取对象锁");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 类锁:修饰静态方法
public static synchronized void staticMethod() {
System.out.println(Thread.currentThread().getName() + " 获取类锁");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
final LockTypeDemo instance1 = new LockTypeDemo();
final LockTypeDemo instance2 = new LockTypeDemo();
// 测试对象锁
new Thread(() -> instance1.instanceMethod(), "Thread-1").start();
new Thread(() -> instance2.instanceMethod(), "Thread-2").start();
// 测试类锁
new Thread(() -> LockTypeDemo.staticMethod(), "Thread-3").start();
new Thread(() -> LockTypeDemo.staticMethod(), "Thread-4").start();
// 测试对象锁与类锁的互不干扰
new Thread(() -> {
instance1.instanceMethod(); // 获取对象锁
}, "Thread-5").start();
new Thread(() -> {
LockTypeDemo.staticMethod(); // 获取类锁
}, "Thread-6").start();
}
}
运行结果表明:
- 不同对象的对象锁互不干扰(Thread-1 和 Thread-2 可同时执行)
- 所有类锁是同一把锁(Thread-3 和 Thread-4 互斥)
- 对象锁和类锁互不干扰(Thread-5 和 Thread-6 可同时执行,因为它们锁定的是不同的对象)
类锁和对象锁本质上都是对象锁,只是锁的对象不同:类锁锁的是 Class 对象,而对象锁锁的是实例对象。
六、实战案例分析
案例 1:死锁问题诊断与修复
public class DeadLockDemo {
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (LOCK_A) {
System.out.println("Thread 1: Holding lock A...");
try { Thread.sleep(1000); } catch (Exception e) {}
System.out.println("Thread 1: Waiting for lock B...");
synchronized (LOCK_B) {
System.out.println("Thread 1: Holding lock A & B");
}
}
}).start();
new Thread(() -> {
synchronized (LOCK_B) {
System.out.println("Thread 2: Holding lock B...");
try { Thread.sleep(1000); } catch (Exception e) {}
System.out.println("Thread 2: Waiting for lock A...");
synchronized (LOCK_A) {
System.out.println("Thread 2: Holding lock A & B");
}
}
}).start();
}
}
问题分析:两个线程分别持有一把锁,同时等待对方释放另一把锁,形成死锁。
解决方案:
- 统一锁获取顺序:所有线程按照相同的顺序获取锁(先获取 LOCK_A,再获取 LOCK_B)
- 使用超时机制:使用 ReentrantLock 的 tryLock(timeout)方法,避免无限期等待
- 避免嵌套锁:重构代码,避免在持有一把锁的情况下再获取另一把锁
// 解决方案1:统一锁获取顺序
public class DeadLockSolution1 {
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 两个线程都先获取LOCK_A,再获取LOCK_B
synchronized (LOCK_A) {
System.out.println("Thread 1: Holding lock A...");
try { Thread.sleep(1000); } catch (Exception e) {}
synchronized (LOCK_B) {
System.out.println("Thread 1: Holding lock A & B");
}
}
}).start();
new Thread(() -> {
// 统一按照相同顺序获取锁
synchronized (LOCK_A) {
System.out.println("Thread 2: Holding lock A...");
try { Thread.sleep(1000); } catch (Exception e) {}
synchronized (LOCK_B) {
System.out.println("Thread 2: Holding lock A & B");
}
}
}).start();
}
}
案例 2:性能对比测试
下面通过测试比较不同锁类型的性能差异:
public class LockPerformanceTest {
private static final int THREAD_COUNT = 10;
private static final int LOOP_COUNT = 100000;
// 测试不同锁的性能
public static void main(String[] args) throws Exception {
// 1. 无锁
testNoLock();
// 2. 对象锁(synchronized方法)
testSynchronizedMethod();
// 3. 对象锁(synchronized块)
testSynchronizedBlock();
// 4. 类锁(static synchronized方法)
testStaticSynchronizedMethod();
// 5. 重入锁(ReentrantLock)
testReentrantLock();
}
// 无锁实现(非线程安全)
private static void testNoLock() throws Exception {
long startTime = System.currentTimeMillis();
final Counter counter = new Counter();
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < LOOP_COUNT; j++) {
counter.noLockIncrement();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
long endTime = System.currentTimeMillis();
System.out.println("无锁操作: " + (endTime - startTime) + "ms, 结果: " + counter.noLockCount);
}
// 对象锁(synchronized方法)测试
private static void testSynchronizedMethod() throws Exception {
long startTime = System.currentTimeMillis();
final Counter counter = new Counter();
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < LOOP_COUNT; j++) {
counter.syncMethodIncrement();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
long endTime = System.currentTimeMillis();
System.out.println("synchronized方法: " + (endTime - startTime) + "ms, 结果: " + counter.syncMethodCount);
}
// 对象锁(synchronized块)测试
private static void testSynchronizedBlock() throws Exception {
long startTime = System.currentTimeMillis();
final Counter counter = new Counter();
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < LOOP_COUNT; j++) {
counter.syncBlockIncrement();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
long endTime = System.currentTimeMillis();
System.out.println("synchronized块: " + (endTime - startTime) + "ms, 结果: " + counter.syncBlockCount);
}
// 类锁(static synchronized方法)测试
private static void testStaticSynchronizedMethod() throws Exception {
long startTime = System.currentTimeMillis();
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < LOOP_COUNT; j++) {
Counter.staticSyncMethodIncrement();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
long endTime = System.currentTimeMillis();
System.out.println("静态synchronized方法: " + (endTime - startTime) + "ms, 结果: " + Counter.staticSyncMethodCount);
}
// ReentrantLock测试
private static void testReentrantLock() throws Exception {
long startTime = System.currentTimeMillis();
final Counter counter = new Counter();
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < LOOP_COUNT; j++) {
counter.reentrantLockIncrement();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
long endTime = System.currentTimeMillis();
System.out.println("ReentrantLock: " + (endTime - startTime) + "ms, 结果: " + counter.reentrantLockCount);
}
// 计数器类
static class Counter {
private int noLockCount = 0;
private int syncMethodCount = 0;
private int syncBlockCount = 0;
private static int staticSyncMethodCount = 0;
private final Lock lock = new ReentrantLock();
private int reentrantLockCount = 0;
// 无锁增加
public void noLockIncrement() {
noLockCount++;
}
// synchronized方法
public synchronized void syncMethodIncrement() {
syncMethodCount++;
}
// synchronized块
public void syncBlockIncrement() {
synchronized(this) {
syncBlockCount++;
}
}
// 静态synchronized方法
public static synchronized void staticSyncMethodIncrement() {
staticSyncMethodCount++;
}
// ReentrantLock
public void reentrantLockIncrement() {
lock.lock();
try {
reentrantLockCount++;
} finally {
lock.unlock();
}
}
}
}
典型测试结果(i7 处理器,8GB 内存,JDK 1.8):
- 无锁操作:约 15ms(结果不正确,存在线程安全问题)
- synchronized 方法:约 85ms
- synchronized 块:约 80ms(略优于方法同步)
- 静态 synchronized 方法:约 90ms(类锁竞争相同)
- ReentrantLock:约 75ms
JDK 1.6 后,synchronized 性能得到极大提升,已经接近显式锁 ReentrantLock 的性能。在单线程情况下,偏向锁的性能接近无锁操作;在低竞争情况下,轻量级锁通过自旋避免了线程阻塞/唤醒的开销。
七、synchronized 使用注意事项
- 减小锁粒度:锁定需要同步的代码块,而非整个方法
// 改进前
public synchronized void processData(List<String> data) {
// 准备阶段(无需同步)
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "准备处理数据...");
// 处理数据(需要同步)
for (String item : data) {
// 临界区操作
}
// 收尾工作(无需同步)
System.out.println(threadName + "处理完成");
}
// 改进后
public void processData(List<String> data) {
// 准备阶段(无需同步)
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "准备处理数据...");
// 只同步需要的代码块
synchronized(this) {
for (String item : data) {
// 临界区操作
}
}
// 收尾工作(无需同步)
System.out.println(threadName + "处理完成");
}
- 避免锁对象被修改:使用 final 修饰锁对象
// 错误示例
class UnsafeLock {
private Object lock = new Object();
public void method() {
synchronized(lock) {
// 临界区代码
}
}
public void changeLock() {
lock = new Object(); // 危险!更换锁对象
}
}
// 正确示例
class SafeLock {
private final Object lock = new Object();
public void method() {
synchronized(lock) {
// 临界区代码
}
}
}
- 避免"死等":synchronized 不可中断,考虑使用可中断的 ReentrantLock
- 注意可重入性:synchronized 是可重入锁,同一线程可以多次获取自己持有的锁
public class ReentrantDemo {
public synchronized void outer() {
System.out.println("进入外层方法");
inner(); // 可重入,不会导致死锁
}
public synchronized void inner() {
System.out.println("进入内层方法");
}
}
八、总结
特性 | 偏向锁 | 轻量级锁 | 重量级锁 |
---|---|---|---|
使用场景 | 单线程访问 | 多线程交替访问 | 多线程竞争访问 |
竞争机制 | 无竞争 | CAS 自旋 | 互斥量+线程阻塞 |
性能开销 | 最低 | 较低 | 最高 |
优点 | 单线程性能最佳 | 无需线程阻塞唤醒 | 稳定性好 |
缺点 | 只适合单线程 | 自旋消耗 CPU | 线程阻塞+上下文切换 |
标志位 | 01+1(偏向标记),存储线程 ID+epoch | 00,Mark Word 存储指向栈中锁记录的指针 | 10,Mark Word 存储指向 Monitor 对象的指针 |
synchronized 作为 Java 多线程编程中最基础的同步机制,经过 JDK 不断优化已经具备很好的性能。理解其底层原理和锁升级过程,有助于我们更高效地使用它,也为解决实际并发问题提供思路。在实际开发中,应结合具体场景选择合适的同步策略,减小锁粒度,避免竞争,从而构建高性能的并发应用。
在下一篇文章中,我们将探讨 ReentrantLock 高级特性与应用,敬请期待!
感谢您耐心阅读到这里!如果觉得本文对您有帮助,欢迎点赞 👍、收藏 ⭐、分享给需要的朋友,您的支持是我持续输出技术干货的最大动力!
如果想获取更多 Java 技术深度解析,欢迎点击头像关注我,后续会每日更新高质量技术文章,陪您一起进阶成长~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。