一、引言
synchronized 是 Java 并发编程中最基础且核心的同步机制,用于保证临界区代码的原子性、可见性和有序性。早期 synchronized 因性能开销较大被称为 "重量级锁",但 JVM 通过偏向锁、轻量级锁、重量级锁的三级锁机制进行了深度优化,在不同并发场景下自动切换锁状态,平衡了线程安全与执行效率。本文基于 Java 并发编程核心知识,结合具体代码示例与 JVM 底层实现,详细拆解 synchronized 的优化原理与实践逻辑。
二、synchronized 核心基础
2.1 核心作用
- 互斥性:同一时刻仅允许一个线程进入临界区,避免竞态条件。
- 可见性:线程释放锁时,会将工作内存中修改的共享变量同步至主存;线程获取锁时,会清空工作内存中共享变量的缓存,从主存重新读取。
- 有序性:禁止指令重排,保证临界区代码按顺序执行。
2.2 底层依赖:Monitor 机制
synchronized 的同步语义依赖Java 对象头 和Monitor(管程) 实现:
- Java 对象头结构 :
- 普通对象的对象头包含 Mark Word(32 位 / 64 位)和 Klass Word(类指针)。
- Mark Word 是核心,存储对象的锁状态、哈希码、年龄代、偏向线程 ID 等信息,不同锁状态下结构不同。
- Monitor 原理 :
- Monitor 是操作系统级别的同步原语,每个 Java 对象都关联一个 Monitor(通过对象头的 Mark Word 指向)。
- Monitor 包含 EntryList(等待锁的线程队列)、Owner(持有锁的线程)、WaitSet(调用 wait () 后等待的线程队列)。
- 线程获取锁时进入 EntryList 阻塞,获取锁后成为 Owner;释放锁时唤醒 EntryList 中的线程竞争锁。
2.3 基础使用代码示例
synchronized 可修饰实例方法、静态方法和代码块,以下是三种使用方式的核心示例:
java
public class SynchronizedBasicDemo {
// 1. 修饰实例方法(锁对象:当前实例this)
public synchronized void instanceMethod() {
System.out.println("实例方法同步:" + Thread.currentThread().getName());
try {
Thread.sleep(100); // 模拟业务耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 2. 修饰静态方法(锁对象:SynchronizedBasicDemo.class)
public static synchronized void staticMethod() {
System.out.println("静态方法同步:" + Thread.currentThread().getName());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 3. 修饰代码块(灵活指定锁对象)
private final Object lock = new Object(); // 自定义锁对象
public void codeBlockMethod() {
synchronized (lock) { // 锁自定义对象
System.out.println("代码块同步(自定义锁):" + Thread.currentThread().getName());
}
synchronized (this) { // 锁当前实例
System.out.println("代码块同步(this锁):" + Thread.currentThread().getName());
}
synchronized (SynchronizedBasicDemo.class) { // 锁类对象
System.out.println("代码块同步(类锁):" + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
SynchronizedBasicDemo demo = new SynchronizedBasicDemo();
// 测试实例方法(同一实例互斥,不同实例不互斥)
new Thread(demo::instanceMethod, "实例线程1").start();
new Thread(demo::instanceMethod, "实例线程2").start();
new Thread(new SynchronizedBasicDemo()::instanceMethod, "实例线程3").start();
// 测试静态方法(全局互斥)
new Thread(SynchronizedBasicDemo::staticMethod, "静态线程1").start();
new Thread(SynchronizedBasicDemo::staticMethod, "静态线程2").start();
// 测试代码块
new Thread(demo::codeBlockMethod, "代码块线程").start();
}
}
关键说明:
- 实例方法锁仅对同一实例的线程互斥,不同实例互不影响;静态方法锁和类对象锁是全局唯一的,所有线程互斥。
- 代码块锁的灵活性最高,可通过指定锁对象缩小同步范围,提升性能。
三、synchronized 三级锁优化机制
JVM 根据并发竞争强度,自动切换 synchronized 的锁状态,从低开销到高开销依次为:偏向锁 → 轻量级锁 → 重量级锁,以下结合代码示例解析各阶段特性。
3.1 偏向锁:无竞争场景优化
3.1.1 设计思想
大多数场景下,锁由同一线程多次获取,无并发竞争。偏向锁通过 "标记线程 ID" 的方式,避免每次获取锁都进行 CAS 操作,降低开销。
3.1.2 实现原理与代码示例
java
import org.openjdk.jol.info.ClassLayout;
public class BiasLockDemo {
private static final Object lock = new Object();
public static void main(String[] args) {
// JVM默认延迟4秒启用偏向锁,此处设置延迟为0(需添加JVM参数:-XX:BiasedLockingStartupDelay=0)
System.out.println("初始状态(无锁):");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
// 单线程多次获取锁,触发偏向锁
synchronized (lock) {
System.out.println("\n第一次加锁(偏向锁):");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
// 同一线程再次获取锁,偏向锁直接生效
synchronized (lock) {
System.out.println("\n第二次加锁(偏向锁复用):");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}
}
运行结果关键信息:
- 无锁状态:Mark Word 中锁标志位为 01,偏向标识位为 0。
- 偏向锁状态:Mark Word 中写入当前线程 ID,偏向标识位变为 1,锁标志位仍为 01。
3.1.3 偏向锁撤销与批量优化
当其他线程尝试竞争偏向锁时,会触发撤销流程,代码示例如下:
java
public class BiasLockRevokeDemo {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
// 线程1先获取锁,触发偏向锁
Thread t1 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程1持有偏向锁");
try {
Thread.sleep(1000); // 保持锁持有状态
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "Thread-1");
t1.start();
t1.join(); // 等待线程1释放锁
// 线程2尝试获取锁,触发偏向锁撤销
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程2竞争锁,偏向锁撤销");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}, "Thread-2");
t2.start();
}
}
关键说明:
- 线程 2 竞争时,JVM 会在安全点暂停线程 1,将偏向锁撤销为无锁或轻量级锁。
- 批量重刻名:当一个类的偏向锁撤销达 20 次,JVM 会将该类所有对象批量偏向当前线程;达 40 次则批量撤销偏向锁。
3.2 轻量级锁:低并发竞争优化
3.2.1 适用场景
多个线程交替获取锁,无长时间持有锁的情况,避免重量级锁的内核态切换开销。
3.2.2 实现原理与代码示例
java
public class LightweightLockDemo {
private static final Object lock = new Object();
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
// 两个线程交替获取锁,触发轻量级锁
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
synchronized (lock) {
count++; // 临界区代码(执行时间短)
}
}
}, "Thread-1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
synchronized (lock) {
count++;
}
}
}, "Thread-2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终计数:" + count);
System.out.println("锁状态(轻量级锁):");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}
关键说明:
- 线程交替执行短临界区代码,竞争程度低,锁状态保持为轻量级锁。
- 轻量级锁的 Mark Word 会存储指向线程栈帧中 "锁记录(Lock Record)" 的指针,锁标志位为 00。
3.2.3 自旋优化
轻量级锁竞争时,线程会通过自旋尝试获取锁,代码示例如下(模拟自旋场景):
java
public class SpinLockDemo {
private static final Object lock = new Object();
private static int count = 0;
public static void main(String[] args) {
// 3个线程交替竞争锁,自旋优化生效
for (int i = 0; i < 3; i++) {
new Thread(() -> {
for (int j = 0; j < 500; j++) {
synchronized (lock) {
count++;
// 临界区执行时间短,自旋可成功获取锁
}
}
System.out.println(Thread.currentThread().getName() + "执行完成");
}, "Thread-" + i).start();
}
}
}
关键说明:
- 自旋次数由 JVM 自适应调整(基于 CPU 核心数和并发情况)。
- 若临界区执行时间短,自旋可避免线程阻塞,提升效率;若执行时间长,自旋会浪费 CPU,触发锁膨胀。
3.3 重量级锁:高并发竞争场景
3.3.1 适用场景
多个线程同时竞争锁,且锁持有时间较长,自旋优化无法提升效率。
3.3.2 实现原理与代码示例
java
public class HeavyweightLockDemo {
private static final Object lock = new Object();
private static int count = 0;
public static void main(String[] args) {
// 10个线程同时竞争锁,且临界区执行时间长,触发重量级锁
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 100; j++) {
synchronized (lock) {
count++;
try {
Thread.sleep(50); // 模拟长时间持有锁
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
System.out.println(Thread.currentThread().getName() + "执行完成");
}, "Thread-" + i).start();
}
}
}
运行结果关键信息:
- 锁状态升级为重量级后,Mark Word 会指向 Monitor 对象,锁标志位为 10。
- 未获取锁的线程进入 EntryList 阻塞(内核态),避免自旋浪费 CPU。
3.3.3 开销分析
- 重量级锁的获取和释放涉及内核态与用户态切换,开销较大。
- 但能保证高并发场景下的线程安全,适合临界区代码执行时间长的场景。
四、锁状态转换流程
4.1 转换触发条件
| 锁状态转换 | 触发场景 |
|---|---|
| 无锁 → 偏向锁 | 线程首次获取锁,无竞争 |
| 偏向锁 → 轻量级锁 | 其他线程尝试竞争偏向锁 |
| 偏向锁 → 无锁 | 持有偏向锁的线程执行完毕,无其他线程竞争 |
| 轻量级锁 → 重量级锁 | 自旋失败,或多个线程同时竞争轻量级锁 |
| 轻量级锁 → 无锁 | 线程释放锁,无其他线程竞争 |
| 重量级锁 → 无锁 | 线程释放锁,无其他线程竞争 |
4.2 轻量级锁→重量级锁的具体膨胀流程(结合示例)
当轻量级锁竞争超过自旋阈值后,会触发锁膨胀,具体流程如下:
- 竞争失败触发膨胀:Thread-1 尝试获取轻量级锁失败(说明自旋失效、存在持续竞争),进入锁膨胀流程。
- 绑定 Monitor 锁:为 Object 对象申请对应的 Monitor 锁,将 Object 对象头的 Mark Word 替换为该 Monitor 的地址(此时锁状态标记为 "重量级锁")。
- 线程阻塞入队 :Thread-1 进入 Monitor 的 EntryList 队列,线程状态变为
BLOCKED。 - 重量级解锁流程 :
- Thread-0 执行完临界区代码,尝试用 CAS 恢复 Object 的 Mark Word(轻量级锁的解锁逻辑),但此时 Object 头已指向 Monitor,CAS 失败。
- Thread-0 切换至重量级解锁:通过 Object 头的 Monitor 地址找到对应的 Monitor 对象,将 Monitor 的
Owner设置为null,同时唤醒 EntryList 中处于BLOCKED状态的线程(如 Thread-1)参与锁竞争。
4.3 完整转换流程图

五、其他优化:锁消除与锁粗化
5.1 锁消除
JVM 通过逃逸分析,识别出不会被多线程共享的局部对象,自动消除其 synchronized 锁。
代码示例
java
public class LockEliminationDemo {
// 局部对象仅当前线程使用,JVM会消除synchronized锁
public String processString(String str) {
// new Object()是局部对象,无逃逸,锁被消除
synchronized (new Object()) {
return str.toUpperCase() + "-" + System.currentTimeMillis();
}
}
public static void main(String[] args) {
LockEliminationDemo demo = new LockEliminationDemo();
// 单线程执行,锁消除优化生效
for (int i = 0; i < 10000; i++) {
demo.processString("test" + i);
}
System.out.println("执行完成(锁消除已生效)");
}
}
关键说明:
- 可通过 JVM 参数
-XX:+EliminateLocks开启锁消除(JDK8 默认开启),-XX:-EliminateLocks禁用。 - 示例中
new Object()创建的对象仅在方法内使用,无线程共享,锁操作冗余,JVM 会自动消除。
5.2 锁粗化
将多个连续的细粒度锁合并为一个粗粒度锁,减少锁获取和释放的次数。
代码示例
java
public class LockCoarseningDemo {
private final Object lock = new Object();
private StringBuilder sb = new StringBuilder();
// 循环内多次获取锁,JVM会将锁粗化到循环外部
public void appendStrings(String... strs) {
for (String str : strs) {
synchronized (lock) { // 细粒度锁
sb.append(str);
}
}
}
public static void main(String[] args) {
LockCoarseningDemo demo = new LockCoarseningDemo();
demo.appendStrings("a", "b", "c", "d", "e");
System.out.println(demo.sb.toString());
}
}
关键说明:
- 未优化前,循环内每次 append 都要获取和释放锁,开销较大。
- JVM 锁粗化后,会在循环开始前获取一次锁,循环结束后释放,减少锁操作次数。
六、优化效果对比
| 锁状态 | 适用场景 | 获取锁开销 | 释放锁开销 | 并发性能 | 代码特征 |
|---|---|---|---|---|---|
| 偏向锁 | 单线程重复获取 | 极低(仅检查线程 ID) | 无(不主动释放) | 最高 | 单线程执行同步块,无竞争 |
| 轻量级锁 | 低并发交替获取 | 低(CAS 操作) | 低(CAS 操作) | 中 | 多线程交替执行短临界区 |
| 重量级锁 | 高并发竞争 | 高(内核态阻塞) | 高(内核态唤醒) | 低 | 多线程同时执行长临界区 |
七、实践建议
-
避免在临界区执行耗时操作(如 IO、网络请求),否则会导致重量级锁长时间持有,降低并发效率。
-
减少锁粒度:将大临界区拆分为多个小临界区,使用不同的锁对象,提升并发度(如 ConcurrentHashMap 的分段锁思想)。
java// 优化前:一个锁控制多个逻辑 synchronized (lock) { createOrder(); updateStock(); } // 优化后:细粒度锁,逻辑并行执行 synchronized (orderLock) { createOrder(); } synchronized (stockLock) { updateStock(); } -
利用锁优化的特性:单线程场景下 synchronized 开销极低,无需刻意替换为 Lock;高并发场景可结合 Lock(如 ReentrantLock)灵活控制锁行为。
-
禁用偏向锁(可选):如果程序存在大量短时间竞争的场景,可通过 JVM 参数
-XX:-UseBiasedLocking禁用偏向锁,避免偏向锁撤销的开销。