synchronized 深度剖析:从语法到锁升级的完整演进
在 Java 并发编程中,synchronized是最基础也最常用的同步机制。从 JDK 1.0 诞生时的重量级锁,到 JDK 6 引入的锁升级机制(偏向锁→轻量级锁→重量级锁),synchronized的性能不断优化,成为保障线程安全的核心工具。然而,很多开发者对其的理解仍停留在 “加锁关键字” 的表层,对底层实现和锁升级细节知之甚少。本文将从语法使用入手,逐步深入到 JVM 层面的锁机制,解析synchronized如何从低效走向高效,以及在不同场景下的最佳实践。
一、synchronized 的语法使用:锁的三种形态
synchronized的核心作用是实现临界区的互斥访问,即同一时间只有一个线程能执行被保护的代码块。它有三种使用形式,分别对应不同的锁对象。
1.1 修饰实例方法:锁为当前对象实例
当synchronized修饰实例方法时,锁的对象是调用该方法的对象实例。不同实例间的锁相互独立,同一实例的多个synchronized方法共享同一把锁。
public class SynchronizedDemo {// 锁对象为当前SynchronizedDemo实例public synchronized void instanceMethod() {// 临界区代码System.out.println("实例方法同步");}public static void main(String[] args) {SynchronizedDemo demo1 = new SynchronizedDemo();SynchronizedDemo demo2 = new SynchronizedDemo();// 线程1调用demo1的同步方法new Thread(demo1::instanceMethod).start();// 线程2调用demo2的同步方法(与线程1不互斥,因为锁对象不同)new Thread(demo2::instanceMethod).start();}
}
特点:
- 锁的粒度是对象实例,适合保护对象级别的共享资源(如实例变量);
- 若多个线程操作同一个实例,会竞争同一把锁;操作不同实例则无竞争。
1.2 修饰静态方法:锁为类的 Class 对象
synchronized修饰静态方法时,锁的对象是当前类的 Class 对象(全局唯一)。无论创建多少个实例,所有线程调用该静态方法都会竞争同一把锁。
public class SynchronizedStaticDemo {// 锁对象为SynchronizedStaticDemo.classpublic static synchronized void staticMethod() {// 临界区代码System.out.println("静态方法同步");}public static void main(String[] args) {SynchronizedStaticDemo demo1 = new SynchronizedStaticDemo();SynchronizedStaticDemo demo2 = new SynchronizedStaticDemo();// 线程1和线程2竞争同一把锁(Class对象),会互斥执行new Thread(demo1::staticMethod).start();new Thread(demo2::staticMethod).start();}
}
特点:
- 锁的粒度是类级别,适合保护静态变量等全局共享资源;
- 所有实例共享同一把锁,竞争强度高于实例方法锁。
1.3 修饰代码块:锁为指定对象
synchronized代码块通过显式指定锁对象,实现更灵活的同步控制。锁对象可以是任意 Java 对象(推荐使用专门的锁对象,如Object lock = new Object())。
public class SynchronizedBlockDemo {private final Object lock = new Object(); // 显式锁对象private int count = 0;public void increment() {// 锁对象为lock,保护count的修改synchronized (lock) {count++;}}public int getCount() {synchronized (lock) { // 与increment共享同一把锁return count;}}
}
特点:
- 锁的粒度可自定义,能减少锁竞争(如用不同锁保护不同资源);
- 避免了修饰方法时的锁粒度过大问题,是实际开发中推荐的方式。
二、锁升级机制:从偏向锁到重量级锁的演进
JDK 6 之前,synchronized的实现依赖操作系统的互斥量(Mutex),每次加锁解锁都需要在用户态和内核态之间切换,性能开销巨大(因此被称为 “重量级锁”)。JDK 6 为优化其性能,引入了锁升级机制:根据竞争强度,自动从偏向锁升级为轻量级锁,最终升级为重量级锁,实现 “按需分配” 性能开销。
锁升级的核心依据是竞争程度:
- 无竞争:使用偏向锁(几乎无开销);
- 轻度竞争(线程交替执行):使用轻量级锁(自旋等待,避免内核态切换);
- 重度竞争(多线程同时争抢):使用重量级锁(依赖操作系统互斥量)。
锁升级是不可逆的(偏向锁→轻量级锁→重量级锁),一旦升级为重量级锁,就不会再降级。
2.1 偏向锁:无竞争场景的最优解
设计初衷:在多数情况下,锁不仅不存在多线程竞争,还会由同一线程多次获取。偏向锁通过 “偏向” 第一个获取锁的线程,消除无竞争场景下的锁开销。
2.1.1 实现原理
- 加锁:当线程第一次获取锁时,JVM 会将对象头中的Mark Word标记为 “偏向模式”,并记录该线程的 ID。后续该线程再次获取锁时,只需检查 Mark Word 中的线程 ID 是否为当前线程,无需其他操作(几乎零开销)。
- 解锁:偏向锁不会主动释放,只有当其他线程尝试获取锁时,持有偏向锁的线程才会释放锁(触发偏向锁撤销)。
对象头 Mark Word 在偏向锁状态的结构(64 位 JVM):
位信息 | 含义 |
0~1 位 | 锁状态标记(01 表示偏向锁) |
2 位 | 偏向锁标志(1 表示处于偏向模式) |
3~12 位 | 偏向线程 ID |
13~17 位 | epoch(偏向锁的时间戳,用于批量重偏向) |
18~23 位 | 未使用 |
24~63 位 | 对象哈希码(无竞争时延迟计算,偏向锁释放时才生成) |
2.1.2 适用场景
- 单线程重复获取锁的场景(如单线程操作集合);
- 几乎无竞争的环境(如线程私有的同步代码块)。
优势:除第一次获取锁时有轻微开销,后续获取锁几乎无需成本。
劣势:存在锁撤销的开销(当其他线程尝试获取锁时,需要暂停持有偏向锁的线程,检查其状态)。
2.2 轻量级锁:应对线程交替执行的场景
当有其他线程尝试获取偏向锁时,偏向锁会被撤销,升级为轻量级锁。轻量级锁适用于线程交替执行同步代码块的场景,通过自旋避免进入重量级锁。
2.2.1 实现原理
- 加锁:
- 线程获取锁时,先在栈帧中创建锁记录(Lock Record),存储对象头中 Mark Word 的副本(Displaced Mark Word);
- 通过 CAS 操作将对象头的 Mark Word 替换为指向锁记录的指针(表示当前线程持有轻量级锁);
- 若 CAS 成功,当前线程获取锁;若失败(说明存在竞争),则自旋重试(默认自旋次数为 10 次)。
- 解锁:
- 通过 CAS 操作将对象头的 Mark Word 恢复为 Displaced Mark Word;
- 若 CAS 成功,解锁完成;若失败(说明锁已升级为重量级锁),则唤醒等待队列中的线程。
对象头 Mark Word 在轻量级锁状态的结构:
位信息 | 含义 |
0~1 位 | 锁状态标记(00 表示轻量级锁) |
2 位及以上 | 指向栈中锁记录(Lock Record)的指针 |
2.2.2 适用场景
- 线程交替执行同步代码块(如两个线程轮流获取锁);
- 竞争持续时间短(自旋等待能在短时间内获取到锁)。
优势:避免了重量级锁的内核态切换开销,通过自旋在用户态解决竞争。
劣势:自旋会消耗 CPU 资源,若竞争激烈(自旋失败),会升级为重量级锁,反而增加开销。
2.3 重量级锁:多线程并发争抢的最终方案
当轻量级锁的自旋失败(超过最大自旋次数或已有线程自旋),锁会升级为重量级锁。重量级锁依赖操作系统的互斥量(Mutex) 实现,适用于多线程同时争抢锁的场景。
2.3.1 实现原理
- 加锁:线程获取重量级锁时,若锁已被占用,当前线程会被阻塞并放入等待队列(由操作系统维护),进入内核态等待;
- 解锁:持有锁的线程释放锁后,会唤醒等待队列中的一个或多个线程,使其重新竞争锁。
对象头 Mark Word 在重量级锁状态的结构:
位信息 | 含义 |
0~1 位 | 锁状态标记(10 表示重量级锁) |
2 位及以上 | 指向操作系统互斥量(Mutex)的指针 |
2.3.2 适用场景
- 多线程同时竞争锁(如高并发场景下的资源争抢);
- 同步代码块执行时间长(自旋等待得不偿失)。
优势:适合重度竞争场景,不会浪费 CPU 资源(线程阻塞时不消耗 CPU)。
劣势:线程阻塞和唤醒需要在用户态和内核态之间切换,开销巨大(约为轻量级锁的 10~100 倍)。
2.4 锁升级的完整流程示例
public class LockUpgradeDemo {private static final Object lock = new Object();public static void main(String[] args) {// 阶段1:单线程获取锁,使用偏向锁new Thread(() -> {synchronized (lock) {System.out.println("线程1获取锁(偏向锁)");try { Thread.sleep(100); } catch (InterruptedException e) {}}}).start();// 阶段2:线程1释放锁后,线程2尝试获取,偏向锁撤销,升级为轻量级锁new Thread(() -> {try { Thread.sleep(200); } catch (InterruptedException e) {} // 等待线程1释放synchronized (lock) {System.out.println("线程2获取锁(轻量级锁)");try { Thread.sleep(100); } catch (InterruptedException e) {}}}).start();// 阶段3:线程2未释放时,线程3尝试获取,轻量级锁升级为重量级锁new Thread(() -> {try { Thread.sleep(250); } catch (InterruptedException e) {} // 线程2持有锁时争抢synchronized (lock) {System.out.println("线程3获取锁(重量级锁)");}}).start();}
}
流程解析:
- 线程 1 首次获取锁,lock对象头变为偏向锁状态,记录线程 1 的 ID;
- 线程 1 释放锁后,线程 2 尝试获取,JVM 撤销偏向锁,升级为轻量级锁,线程 2 通过 CAS 获取锁;
- 线程 2 持有锁时,线程 3 尝试获取,轻量级锁自旋失败,升级为重量级锁,线程 3 进入内核态等待;
- 线程 2 释放锁后,操作系统唤醒线程 3,线程 3 获取重量级锁。
三、synchronized 与其他锁的对比:如何选择?
在 Java 并发包中,ReentrantLock等锁机制也能实现同步功能。了解synchronized与它们的差异,才能在实际开发中做出合理选择。
特性 | synchronized | ReentrantLock |
锁实现 | JVM 层面(C++ 实现) | API 层面(Java 代码实现) |
锁升级 | 支持(偏向锁→轻量级锁→重量级锁) | 不支持,始终是重量级锁(但可通过公平性设置优化) |
可中断 | 不可中断(获取锁时会一直阻塞) | 可中断(tryLock (long timeout, TimeUnit unit)) |
公平性 | 非公平锁(无法设置) | 支持公平锁和非公平锁(构造函数参数) |
条件变量 | 不支持 | 支持(通过 Condition 实现多条件等待) |
性能 | 低竞争时接近 ReentrantLock,高竞争时略差 | 高竞争时性能更稳定 |
最佳实践:
- 简单同步场景(如单例模式、简单计数器):优先使用synchronized(语法简洁,不易出错);
- 复杂场景(如需要中断、超时等待、多条件唤醒):使用ReentrantLock;
- 高并发且竞争激烈的场景:根据测试结果选择(通常ReentrantLock表现更优)。
四、常见误区与性能优化
4.1 误区一:过度使用 synchronized 导致性能下降
很多开发者为 “安全起见”,盲目扩大synchronized的范围,导致锁竞争加剧。例如:
// 错误示例:同步整个方法,包含无需同步的IO操作
public synchronized void process() {// 1. 无需同步的IO操作(耗时较长)readFile();// 2. 需要同步的共享变量修改count++;
}
优化:缩小同步范围,只同步临界区:
public void process() {readFile(); // 无需同步的操作在锁外执行synchronized (lock) {count++; // 仅同步必要代码}
}
4.2 误区二:认为 synchronized 会导致死锁
synchronized本身不会导致死锁,但多把锁的无序获取会导致死锁。例如:
// 线程1:先获取lockA,再获取lockBsynchronized (lockA) {synchronized (lockB) { ... }}// 线程2:先获取lockB,再获取lockAsynchronized (lockB) {synchronized (lockA) { ... }}
避免方案:
- 所有线程按固定顺序获取锁(如先获取 lockA,再获取 lockB);
- 使用tryLock设置超时时间,避免无限等待。
4.3 性能优化技巧
- 减少锁竞争:
- 拆分锁(将一个大锁拆分为多个小锁,如ConcurrentHashMap的分段锁);
- 使用无锁数据结构(如AtomicInteger替代synchronized计数器)。
- 合理利用偏向锁:
- 单线程场景下,确保偏向锁未被禁用(-XX:+UseBiasedLocking,JDK 6 + 默认开启);
- 避免频繁创建线程导致偏向锁频繁撤销(可通过-XX:BiasedLockingStartupDelay=0取消偏向锁延迟)。
- 控制轻量级锁自旋次数:
- 高 CPU 场景下,可适当增加自旋次数(-XX:PreBlockSpin=20);
- 低 CPU 场景下,减少自旋次数,避免浪费 CPU。
五、总结:synchronized 的进化与未来
从 JDK 1.0 的重量级锁到 JDK 6 的锁升级机制,synchronized的进化史就是 Java 并发性能优化的缩影。它的核心价值在于简单可靠—— 即使是新手也能通过它写出线程安全的代码,而锁升级机制又为其在高并发场景下的性能提供了保障。
理解synchronized的关键不仅在于其语法使用,更在于掌握锁升级的底层逻辑:
- 偏向锁是 “无竞争时的偷懒策略”,最大化减少无竞争开销;
- 轻量级锁是 “轻度竞争时的折中方案”,用自旋换取内核态切换成本;
- 重量级锁是 “重度竞争时的无奈之举”,通过操作系统机制保证线程安全。
在实际开发中,没有 “最优” 的锁,只有 “最合适” 的锁。根据业务场景的竞争强度选择同步机制,才能在安全性和性能之间找到最佳平衡。下一篇文章,我们将深入探讨Lock接口及其实现类,对比其与synchronized的设计差异,揭示 Java 并发工具的更多可能性。