synchronized 深度剖析:从语法到锁升级的完整演进
在 Java 并发编程领域,synchronized 无疑是最基础、最经典的同步机制。从 JDK 1.0 诞生时的“重量级锁”,到 JDK 6 引入的锁升级机制(偏向锁→轻量级锁→重量级锁),synchronized 历经多代优化,从“性能鸡肋”蜕变为“高效可靠”的线程安全保障工具。然而,很多开发者对它的理解仅停留在“加锁关键字”的表层,能熟练使用却不懂底层实现,能应对简单场景却无法优化高并发问题。
本文将从语法使用入手,逐步深入 JVM 底层实现,拆解锁升级的完整流程,对比其他锁机制的差异,梳理常见误区与优化技巧,既是面向新手的入门指南,也是面向开发者的进阶解析,助力大家真正吃透 synchronized,从容应对面试与实战。
一、synchronized 的语法使用:锁的三种形态,按需选择更高效
synchronized 的核心作用是实现“临界区互斥访问”,即同一时间只有一个线程能执行被保护的代码块,从而避免多线程并发修改共享资源导致的数据不一致问题。它有三种使用形式,分别对应不同的锁对象和应用场景,掌握其差异是正确使用的前提。
1.1 修饰实例方法:锁为当前对象实例
当 synchronized 修饰实例方法时,锁的对象是调用该方法的对象实例。这意味着,不同对象实例之间的锁相互独立,互不干扰;而同一个对象实例的所有 synchronized 修饰的实例方法,会共享同一把锁。
实战示例(含详细注释):
public class SynchronizedDemo {
// 锁对象为当前SynchronizedDemo实例(this)
public synchronized void instanceMethod() {
// 临界区代码:操作实例级共享资源
System.out.println("实例方法同步,当前线程:" + Thread.currentThread().getName());
try {
// 模拟业务执行耗时
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 同一实例的另一个synchronized实例方法,共享同一把锁
public synchronized void anotherInstanceMethod() {
System.out.println("另一实例方法同步,当前线程:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
SynchronizedDemo demo1 = new SynchronizedDemo();
SynchronizedDemo demo2 = new SynchronizedDemo();
// 线程1调用demo1的instanceMethod
new Thread(() -> demo1.instanceMethod(), "线程1").start();
// 线程2调用demo1的anotherInstanceMethod(与线程1互斥,因为锁对象都是demo1)
new Thread(() -> demo1.anotherInstanceMethod(), "线程2").start();
// 线程3调用demo2的instanceMethod(与线程1、2不互斥,锁对象是demo2)
new Thread(() -> demo2.instanceMethod(), "线程3").start();
}
}
核心特点与适用场景:
- 锁粒度为“对象实例”,适合保护实例级共享资源(如实例变量、对象的状态);
- 多线程操作同一个实例时,会竞争同一把锁,串行执行;操作不同实例时,无锁竞争,并行执行;
- 简洁易用,但锁粒度较粗,若实例中存在多个独立的共享资源,可能导致不必要的锁竞争。
1.2 修饰静态方法:锁为类的 Class 对象
当 synchronized 修饰静态方法时,锁的对象不再是具体的实例,而是当前类的 Class 对象(每个类在 JVM 中只有一个 Class 对象,全局唯一)。这意味着,无论创建多少个类的实例,所有线程调用该静态方法时,都会竞争同一把锁。
实战示例:
public class SynchronizedStaticDemo {
// 锁对象为SynchronizedStaticDemo.class(全局唯一)
public static synchronized void staticMethod() {
System.out.println("静态方法同步,当前线程:" + Thread.currentThread().getName());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SynchronizedStaticDemo demo1 = new SynchronizedStaticDemo();
SynchronizedStaticDemo demo2 = new SynchronizedStaticDemo();
// 线程1调用demo1的静态方法
new Thread(() -> demo1.staticMethod(), "线程1").start();
// 线程2调用demo2的静态方法(与线程1互斥,锁对象都是Class对象)
new Thread(() -> demo2.staticMethod(), "线程2").start();
// 线程3直接调用静态方法(同样与线程1、2互斥)
new Thread(() -> SynchronizedStaticDemo.staticMethod(), "线程3").start();
}
}
一键获取完整项目代码(含测试用例)
核心特点与适用场景:
- 锁粒度为“类级别”,适合保护全局共享资源(如静态变量、工具类的静态方法);
- 所有实例共享同一把锁,锁竞争强度高于实例方法锁;
- 注意:静态 synchronized 方法与实例 synchronized 方法,锁对象不同,互不干扰(即使是同一个实例,调用两种方法也不会互斥)。
1.3 修饰代码块:锁为指定对象,灵活控制锁粒度
synchronized 代码块是最灵活的使用形式,通过显式指定“锁对象”,可以精准控制同步的范围和锁粒度,避免修饰方法时的锁粒度过大问题,是实际开发中最推荐的使用方式。
锁对象可以是任意 Java 对象(推荐使用专门的锁对象,如 Object lock = new Object(),避免使用 this 或 Class 对象导致的锁竞争扩大)。
实战示例(优化版计数器,避免锁粒度浪费):
public class SynchronizedBlockDemo {
// 显式定义锁对象,推荐使用final,避免锁对象被修改导致锁失效
private final Object countLock = new Object();
private int count = 0;
// 独立的锁对象,保护另一个共享资源
private final Object infoLock = new Object();
private String info = "";
// 同步代码块,仅保护count的修改
public void increment() {
// 只对临界区加锁,非临界区代码不参与同步
synchronized (countLock) {
count++;
System.out.println("计数器更新:" + count);
}
}
// 同步代码块,与increment共享同一把锁(countLock)
public int getCount() {
synchronized (countLock) {
return count;
}
}
// 使用另一把锁,保护info,与count的锁互不干扰
public void setInfo(String newInfo) {
synchronized (infoLock) {
this.info = newInfo;
System.out.println("信息更新:" + info);
}
}
public static void main(String[] args) {
SynchronizedBlockDemo demo = new SynchronizedBlockDemo();
// 线程1操作count
new Thread(() -> {
for (int i = 0; i < 5; i++) {
demo.increment();
}
}, "线程1").start();
// 线程2操作info(与线程1不互斥,锁对象不同)
new Thread(() -> {
for (int i = 0; i < 5; i++) {
demo.setInfo("info-" + i);
}
}, "线程2").start();
}
}
一键获取完整项目代码(含测试用例)
核心特点与适用场景:
- 锁粒度可自定义,能最大限度减少锁竞争(如用不同锁保护不同的共享资源,实现“细粒度锁”);
- 可灵活控制同步范围,仅对“临界区代码”加锁,避免非临界区代码(如 IO 操作、耗时计算)占用锁资源;
- 锁对象必须是“不可变”的(推荐用 final 修饰),否则锁对象被修改后,会导致不同线程持有不同的锁,失去同步效果。
二、锁升级机制:从偏向锁到重量级锁的演进,读懂 JVM 的性能优化逻辑
JDK 6 之前,synchronized 的实现完全依赖操作系统的“互斥量(Mutex)”,每次加锁、解锁都需要在“用户态”和“内核态”之间切换——这种切换的开销巨大(一次切换约消耗 1000+ CPU 时钟周期),因此当时的 synchronized 被称为“重量级锁”,性能较差,甚至被开发者避而远之。
JDK 6 为了解决这个问题,引入了“锁升级”机制:JVM 会根据“锁的竞争强度”,自动将锁从“偏向锁”升级为“轻量级锁”,最终升级为“重量级锁”,实现“按需分配”性能开销,让 synchronized 在不同竞争场景下都能保持高效。
核心原则:锁升级是不可逆的(偏向锁→轻量级锁→重量级锁),一旦升级为重量级锁,就不会再降级为轻量级锁或偏向锁——这是因为锁升级的触发条件是“竞争加剧”,而竞争缓解后,降级的收益远小于实现成本。
2.1 偏向锁:无竞争场景的最优解,“偷懒”的高效策略
设计初衷:JVM 统计发现,在多数实际场景中,锁不仅不存在多线程竞争,还会由同一线程多次获取(比如单线程操作同步代码块、循环调用同步方法)。偏向锁的核心就是“偏向”第一个获取锁的线程,消除无竞争场景下的锁开销。
2.1.1 实现原理(结合对象头 Mark Word)
要理解偏向锁,首先要明确:Java 中每个对象都有一个“对象头”(Object Header),其中最关键的部分是“Mark Word”(标记字),它存储了对象的锁状态、哈希码、线程 ID 等信息。64 位 JVM 中,Mark Word 的默认结构如下(无锁状态):
偏向锁的实现流程:
- 加锁:线程第一次获取锁时,JVM 会将对象头 Mark Word 的“锁状态标记”设为 01(偏向锁),“偏向锁标志”设为 1,同时记录当前线程的 ID(3~12 位)和 epoch(偏向锁时间戳,13~17 位);
- 重入锁:后续该线程再次获取锁时,只需检查 Mark Word 中的线程 ID 是否为当前线程——如果是,直接获取锁,无需任何 CAS 操作(几乎零开销);
- 解锁:偏向锁不会主动释放,只有当其他线程尝试获取该锁时,持有偏向锁的线程才会释放锁(触发“偏向锁撤销”)。
64 位 JVM 中,偏向锁状态下 Mark Word 的结构:
| 位信息 | 含义 |
|---|---|
| 0~1 位 | 锁状态标记(01 表示偏向锁) |
| 2 位 | 偏向锁标志(1 表示处于偏向模式) |
| 3~12 位 | 偏向线程 ID(持有偏向锁的线程 ID) |
| 13~17 位 | epoch(偏向锁时间戳,用于批量重偏向) |
| 18~23 位 | 未使用 |
| 24~63 位 | 对象哈希码(无竞争时延迟计算,偏向锁释放时才生成) |
2.1.2 适用场景与优缺点
适用场景:
- 单线程重复获取锁的场景(如单线程操作集合、循环调用同步方法);
- 几乎无竞争的环境(如线程私有的同步代码块、低并发场景)。
优势:除第一次获取锁时有轻微的 CAS 操作开销,后续获取锁几乎无需成本,性能接近无锁状态。
劣势:存在“偏向锁撤销”的开销——当其他线程尝试获取锁时,JVM 需要暂停持有偏向锁的线程,检查其状态(是否还在执行同步代码),若线程已退出同步代码,则撤销偏向锁,升级为轻量级锁;若线程仍在执行,则直接升级为重量级锁。
补充:JDK 6+ 默认开启偏向锁,可通过 JVM 参数控制:-XX:+UseBiasedLocking(开启,默认)、-XX:-UseBiasedLocking(关闭);偏向锁默认有延迟(约 4 秒),可通过 -XX:BiasedLockingStartupDelay=0 取消延迟,适合单线程启动后立即使用同步的场景。
2.2 轻量级锁:轻度竞争的折中方案,用自旋换效率
当有其他线程尝试获取偏向锁时,偏向锁会被撤销,锁升级为“轻量级锁”。轻量级锁适用于“线程交替执行同步代码块”的场景(轻度竞争),核心是通过“自旋等待”避免进入重量级锁的内核态切换。
2.2.1 实现原理(结合栈帧锁记录)
轻量级锁的实现依赖“栈帧中的锁记录(Lock Record)”和 CAS 操作,流程如下:
加锁流程:
- 线程获取锁时,先在自己的栈帧中创建一个“锁记录(Lock Record)”,存储当前对象头 Mark Word 的副本(称为 Displaced Mark Word);
- 通过 CAS 操作,将对象头的 Mark Word 替换为“指向当前锁记录的指针”;
- 若 CAS 操作成功,说明当前线程获取到轻量级锁,继续执行同步代码;
- 若 CAS 操作失败,说明存在其他线程竞争锁(轻度竞争),当前线程会进入“自旋等待”(默认自旋 10 次),尝试再次执行 CAS 操作。
解锁流程:
- 线程执行完同步代码后,通过 CAS 操作,将对象头的 Mark Word 恢复为 Displaced Mark Word;
- 若 CAS 操作成功,说明解锁完成,没有其他线程竞争锁;
- 若 CAS 操作失败,说明锁已被其他线程竞争,当前锁已升级为重量级锁,此时需要唤醒等待队列中的线程。
64 位 JVM 中,轻量级锁状态下 Mark Word 的结构:
| 位信息 | 含义 |
|---|---|
| 0~1 位 | 锁状态标记(00 表示轻量级锁) |
| 2 位及以上 | 指向栈中锁记录(Lock Record)的指针 |
2.2.2 适用场景与优缺点
适用场景:
- 线程交替执行同步代码块(如两个线程轮流获取锁,无同时争抢);
- 竞争持续时间短(自旋等待能在短时间内获取到锁)。
优势:避免了重量级锁的内核态切换开销,通过自旋在用户态解决轻度竞争,性能远高于重量级锁。
劣势:自旋会消耗 CPU 资源——如果竞争激烈(自旋多次仍无法获取锁),会导致 CPU 使用率飙升,此时锁会升级为重量级锁,反而增加整体开销。
补充:轻量级锁的自旋次数可通过 JVM 参数控制:-XX:PreBlockSpin=10(默认 10 次);JDK 1.7 后引入“自适应自旋”,JVM 会根据历史自旋成功率,动态调整自旋次数(如自旋成功次数多,则增加自旋次数;失败次数多,则减少或直接升级为重量级锁)。
2.3 重量级锁:重度竞争的最终方案,依赖操作系统保障安全
当轻量级锁的自旋失败(超过最大自旋次数,或已有多个线程自旋),说明锁的竞争进入“重度阶段”(多线程同时争抢锁),此时锁会升级为“重量级锁”。重量级锁依赖操作系统的“互斥量(Mutex)”实现,线程会进入内核态阻塞,彻底解决并发争抢问题。
2.3.1 实现原理(结合操作系统互斥量)
重量级锁的核心是“将线程阻塞到内核态”,由操作系统负责线程的调度和唤醒,流程如下:
- 加锁:线程获取重量级锁时,若锁已被其他线程占用,当前线程会被阻塞,并放入操作系统维护的“等待队列”(阻塞队列),从用户态切换到内核态,不再消耗 CPU 资源;
- 解锁:持有锁的线程执行完同步代码后,释放锁,并通过操作系统唤醒等待队列中的一个或多个线程,被唤醒的线程重新竞争锁(非公平锁,默认)。
64 位 JVM 中,重量级锁状态下 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) throws InterruptedException {
// 阶段1:单线程获取锁,使用偏向锁
Thread thread1 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程1获取锁(当前锁状态:偏向锁)");
try {
// 模拟业务执行,让线程1持有锁一段时间
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "线程1");
thread1.start();
thread1.join(); // 等待线程1执行完毕,释放锁
// 阶段2:线程2尝试获取锁,偏向锁撤销,升级为轻量级锁
Thread thread2 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程2获取锁(当前锁状态:轻量级锁)");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "线程2");
thread2.start();
// 阶段3:线程2未释放锁时,线程3尝试获取,轻量级锁升级为重量级锁
Thread.sleep(50); // 确保线程2已获取锁
Thread thread3 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程3获取锁(当前锁状态:重量级锁)");
}
}, "线程3");
thread3.start();
}
}
一键获取完整项目代码(含锁状态监控工具)
流程解析(结合 JVM 监控工具验证):
- 线程1首次获取锁:lock 对象从“无锁状态”变为“偏向锁”,Mark Word 记录线程1的 ID;
- 线程1释放锁:偏向锁未主动释放,Mark Word 仍记录线程1的 ID;
- 线程2尝试获取锁:JVM 检测到有其他线程竞争,撤销偏向锁,lock 对象升级为“轻量级锁”,线程2通过 CAS 操作获取锁;
- 线程3尝试获取锁:此时线程2仍持有轻量级锁,线程3自旋等待失败,轻量级锁升级为“重量级锁”,线程3进入内核态阻塞;
- 线程2释放锁:操作系统唤醒线程3,线程3获取重量级锁,执行同步代码。
补充:可通过 JDK 自带的 jvisualvm 工具,监控锁状态变化(查看“线程”→“锁”标签页),直观观察锁升级的过程。
三、synchronized 与其他锁的对比:实战场景如何选择?
在 Java 并发包中,除了 synchronized,还有 ReentrantLock、ReadWriteLock 等锁机制,它们各有优势,了解其差异才能在实际开发中做出合理选择。以下重点对比 synchronized 与最常用的 ReentrantLock(可重入锁):
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 锁实现层面 | JVM 层面(C++ 底层实现) | API 层面(Java 代码实现,基于 AQS 框架) |
| 锁升级机制 | 支持(偏向锁→轻量级锁→重量级锁),自适应调整 | 不支持,始终是重量级锁(但可通过公平性设置优化) |
| 可中断性 | 不可中断(获取锁时会一直阻塞,除非线程被中断) | 可中断(通过 tryLock(long timeout, TimeUnit unit) 实现超时中断,或 lockInterruptibly() 实现可中断) |
| 公平性 | 非公平锁(默认,无法设置为公平锁) | 支持公平锁和非公平锁(通过构造函数参数控制:new ReentrantLock(true) 为公平锁) |
| 条件变量 | 不支持(无法实现多条件等待) | 支持(通过 Condition 接口实现多条件等待,可唤醒指定条件的线程) |
| 性能表现 | 低竞争时接近 ReentrantLock,高竞争时略差(锁升级后仍有内核态切换开销) | 高竞争时性能更稳定(基于 AQS 框架,避免不必要的内核态切换) |
| 易用性 | 语法简洁,无需手动释放锁(JVM 自动释放),不易出错 | 语法复杂,需手动释放锁(必须在 finally 块中释放),否则会导致死锁 |
最佳实践建议:
- 简单同步场景(如单例模式、简单计数器、少量共享资源修改):优先使用 synchronized——语法简洁,不易出错,JVM 自动优化,性能足够满足需求;
- 复杂场景(如需要中断、超时等待、多条件唤醒、公平锁):使用 ReentrantLock——灵活性更高,能应对更复杂的并发场景;
- 高并发且竞争激烈的场景:先通过测试对比两者性能,通常 ReentrantLock 表现更优,但也可通过 synchronized 的锁优化技巧(如细粒度锁)提升性能;
- 读写分离场景(读多写少):使用 ReadWriteLock(ReentrantReadWriteLock),读操作共享锁,写操作独占锁,提升并发效率。
四、常见误区与性能优化:避开坑,提效率
很多开发者使用 synchronized 时,容易陷入一些误区,导致性能下降或线程安全问题。以下梳理最常见的 3 个误区,并给出对应的优化技巧。
4.1 误区一:过度使用 synchronized,扩大同步范围
很多开发者为了“图省事”或“保证安全”,盲目将 synchronized 修饰整个方法,导致同步范围过大,锁竞争加剧,性能下降。尤其是当方法中包含无需同步的耗时操作(如 IO 操作、网络请求)时,会严重浪费锁资源。
错误示例:
// 错误:同步整个方法,包含无需同步的IO操作
public synchronized void processData() {
// 1. 无需同步的IO操作(耗时较长,占用锁资源)
String data = readFile("data.txt");
// 2. 需要同步的共享变量修改(仅这一行需要同步)
sharedCount++;
// 3. 无需同步的耗时计算
process(data);
}
优化方案:缩小同步范围,仅对“临界区代码”(修改共享资源的代码)加锁,非临界区代码放在锁外执行:
public void processData() {
// 无需同步的操作在锁外执行,不占用锁资源
String data = readFile("data.txt");
process(data);
// 仅同步临界区代码,最小化锁持有时间
synchronized (lock) {
sharedCount++;
}
}
4.2 误区二:认为 synchronized 会导致死锁
很多开发者误以为“使用 synchronized 就会导致死锁”,其实不然——synchronized 本身不会导致死锁,多把锁的无序获取才是死锁的根源。当多个线程持有不同的锁,且相互等待对方释放锁时,就会陷入死锁。
死锁示例(synchronized 多锁无序获取):
public class DeadLockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
// 线程1:先获取lockA,再获取lockB
public static void thread1() {
synchronized (lockA) {
System.out.println("线程1持有lockA,尝试获取lockB");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) {
System.out.println("线程1获取lockB,执行完毕");
}
}
}
// 线程2:先获取lockB,再获取lockA
public static void thread2() {
synchronized (lockB) {
System.out.println("线程2持有lockB,尝试获取lockA");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockA) {
System.out.println("线程2获取lockA,执行完毕");
}
}
}
public static void main(String[] args) {
new Thread(DeadLockDemo::thread1, "线程1").start();
new Thread(DeadLockDemo::thread2, "线程2").start();
}
}
避免死锁的方案:
- 所有线程按固定顺序获取锁(如先获取 lockA,再获取 lockB),打破“相互等待”的条件;
- 使用 ReentrantLock 的 tryLock 方法设置超时时间,避免线程无限等待(如 tryLock(100, TimeUnit.MILLISECONDS),超时则放弃获取锁);
- 减少锁的数量,避免同时持有多把锁(如通过拆分资源,减少锁的依赖)。
4.3 性能优化技巧:最大化发挥 synchronized 的性能
除了缩小同步范围,还有以下 3 个实用技巧,可进一步提升 synchronized 的性能:
技巧1:减少锁竞争,实现“细粒度锁”
将一个“大锁”拆分为多个“小锁”,让不同的共享资源使用不同的锁,减少线程间的锁竞争。最典型的例子是 ConcurrentHashMap(JDK 1.7)的“分段锁”——将 HashMap 分为多个段,每个段对应一把锁,不同段的操作互不干扰,提升并发效率。
示例(拆分锁优化):
// 优化前:一个锁保护多个共享资源,竞争激烈
public class BigLockDemo {
private final Object lock = new Object();
private int count1 = 0;
private int count2 = 0;
public void incrementCount1() {
synchronized (lock) { count1++; }
}
public void incrementCount2() {
synchronized (lock) { count2++; }
}
}
// 优化后:拆分锁,每个共享资源对应一把锁,减少竞争
public class SmallLockDemo {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
private int count1 = 0;
private int count2 = 0;
public void incrementCount1() {
synchronized (lock1) { count1++; }
}
public void incrementCount2() {
synchronized (lock2) { count2++; }
}
}
技巧2:合理利用偏向锁,优化单线程场景
在单线程场景下,确保偏向锁未被禁用(JDK 6+ 默认开启),避免频繁创建线程导致偏向锁频繁撤销。如果程序启动后立即需要使用同步,可通过 JVM 参数 -XX:BiasedLockingStartupDelay=0 取消偏向锁的延迟,减少首次获取锁的开销。
注意:如果程序中存在大量多线程竞争场景,可禁用偏向锁(-XX:-UseBiasedLocking),避免偏向锁撤销的开销。
技巧3:控制轻量级锁自旋次数,适配 CPU 场景
轻量级锁的自旋次数会影响性能:高 CPU 场景下,适当增加自旋次数(如 -XX:PreBlockSpin=20),让线程有更多机会获取锁,避免升级为重量级锁;低 CPU 场景下,减少自旋次数(如 -XX:PreBlockSpin=5),避免自旋浪费 CPU 资源。
JDK 1.7 后的“自适应自旋”已能自动适配场景,一般无需手动调整,但在高并发、高 CPU 场景下,可手动优化自旋次数。
五、总结:synchronized 的进化与未来
从 JDK 1.0 的重量级锁,到 JDK 6 的锁升级机制,synchronized 的进化史,就是 Java 并发性能优化的缩影。它的核心价值在于“简单可靠”——即使是新手,也能通过它快速写出线程安全的代码;而锁升级机制,则为它在高并发场景下的性能提供了保障,让它从“被嫌弃”的重量级锁,成为“性价比极高”的同步工具。
理解 synchronized 的关键,不仅在于掌握其三种语法形态,更在于吃透锁升级的底层逻辑:
- 偏向锁是“无竞争时的偷懒策略”,最大化减少无竞争开销,适配单线程场景;
- 轻量级锁是“轻度竞争时的折中方案”,用自旋换取内核态切换成本,适配线程交替执行场景;
- 重量级锁是“重度竞争时的无奈之举”,通过操作系统机制保证线程安全,适配多线程同时争抢场景。
在实际开发中,没有“最优”的锁,只有“最合适”的锁。根据业务场景的竞争强度,选择合适的同步机制,才能在“线程安全”和“性能”之间找到最佳平衡。
下一篇文章,我们将深入探讨 Lock 接口及其实现类(ReentrantLock、ReadWriteLock),对比其与 synchronized 的设计差异,揭示 Java 并发工具的更多可能性。关注专栏,获取更多 Java 并发实战干货!
更多推荐




所有评论(0)