在Java并发编程领域,synchronized无疑是最基础也最核心的同步工具。从JDK早期的“重量级锁”标签,到JDK1.6引入锁升级机制带来的性能飞跃,再到JDK15之后对偏向锁的默认禁用,synchronized的优化从未停止。本文将基于JDK17 LTS版本,从底层对象布局、JVM源码实现几个维度,100%准确地拆解synchronized的锁升级全流程,让你彻底搞懂无锁、偏向锁、轻量级锁、重量级锁的底层逻辑,既能夯实并发编程的核心基础,也能解决生产环境中的实际并发问题。
一、前置核心知识:Java对象布局与Mark Word
整个synchronized锁升级机制,完全是围绕Java对象头中的Mark Word实现的,想要彻底搞懂锁升级,必须先吃透Java对象的内存布局。
在64位JVM(当前生产环境主流)开启压缩指针(-XX:+UseCompressedOops,JDK17默认开启)的情况下,Java对象在堆内存中的布局分为三个部分:
- 对象头(Object Header):分为
Mark Word(标记字段,8字节)、Class Pointer(类型指针,4字节,压缩后);如果是数组对象,额外增加4字节的数组长度。 - 实例数据(Instance Data):对象的成员变量(包括父类继承的),按照8字节对齐规则排列。
- 对齐填充(Padding):保证整个对象的大小是8字节的整数倍,满足JVM的内存对齐要求。
核心重点:Mark Word动态数据结构
Mark Word是一个8字节(64位)的动态数据结构,会根据对象的运行状态复用存储空间,不同状态下的位分布完全不同,这是锁升级的核心载体。以下是64位JVM开启压缩指针后,Mark Word在不同锁状态下的权威位分布(基于OpenJDK 17源码):
| 锁状态 | 64位Mark Word位分布(从高位到低位) | 偏向锁标志(1位) | 锁标志(2位) |
| 无锁不可偏向 | unused(25) | identityHashCode(31) | unused(1) | 分代年龄(4) | 0 | 01 |
| 无锁可偏向 | thread(54) | epoch(2) | unused(1) | 分代年龄(4) | 1 | 01 |
| 偏向锁 | 偏向线程ID(54) | epoch(2) | unused(1) | 分代年龄(4) | 1 | 01 |
| 轻量级锁 | 指向线程栈Lock Record的指针(62) | 无 | 00 |
| 重量级锁 | 指向ObjectMonitor对象的指针(62) | 无 | 10 |
| GC标记 | 空(62) | 无 | 11 |
这里必须明确两个核心判断位,也是很多技术文章的常见错误点:
- lock位(2位):决定锁的基础状态,仅靠这一位无法区分无锁和偏向锁。
- biased_lock位(1位):与lock位配合,
01+0为无锁不可偏向,01+1为可偏向/偏向锁状态。
二、synchronized的使用场景与底层JVM实现
2.1 三种核心使用场景
synchronized的本质是对某个对象加锁,根据锁对象的不同,分为三种使用场景:
- 修饰实例方法:锁对象为当前实例
this,属于对象锁,不同实例的锁互不影响。 - 修饰静态方法:锁对象为当前类的
Class对象,属于类锁,对该类的所有实例生效。 - 修饰代码块:锁对象为括号内指定的对象,可自定义锁粒度,是生产环境推荐的用法。
2.2 底层JVM实现原理
根据JVM规范,synchronized的底层实现分为两种形式,本质都是获取对象对应的监视器锁:
- 同步代码块:通过
monitorenter和monitorexit字节码指令实现。线程执行到monitorenter时尝试获取对象的监视器所有权,执行到monitorexit时释放所有权,编译器会保证异常时也能执行monitorexit,避免死锁。 - 同步方法:通过方法修饰符中的
ACC_SYNCHRONIZED标志实现。JVM调用方法时会检查该标志,若开启则自动尝试获取对象的监视器锁,方法执行完毕后自动释放,语义与monitorenter/monitorexit完全一致。
所有Java对象都天然关联一个ObjectMonitor监视器对象,这是重量级锁的核心载体,而偏向锁、轻量级锁不会直接初始化该对象,这也是不同锁类型的核心性能差异来源。
三、锁升级全流程核心原理
synchronized的锁升级是JVM为了减少同步开销而做的自适应优化,核心逻辑是:根据竞争激烈程度,从低开销锁逐步升级到高开销锁,锁的膨胀过程在持有期间是单向的,不可降级,锁完全释放后会重置为无锁状态。
锁升级全流程总览
3.1 第一阶段:无锁状态
无锁状态分为两种子状态,是锁升级的起点:
- 无锁可偏向状态:开启偏向锁的前提下,对象刚创建,未调用过
System.identityHashCode(),Mark Word中biased_lock=1、lock=01,线程ID、epoch字段全为0,可被第一个获取锁的线程偏向。 - 无锁不可偏向状态:关闭偏向锁,或对象已调用过
identityHashCode(),Mark Word中biased_lock=0、lock=01,存储了对象的哈希值,无法进入偏向锁状态,获取锁时直接进入轻量级锁流程。
核心知识点:为什么调用identityHashCode会禁用偏向锁?
64位JVM中,对象的identityHashCode是31位,正好占用了偏向锁状态下存储线程ID的54位空间的低31位。一旦写入哈希值,就没有足够空间存储偏向线程ID,JVM会强制禁用该对象的偏向锁,这是底层位空间冲突导致的必然结果,后续会通过代码实战验证该逻辑。
3.2 第二阶段:偏向锁
核心设计思想
在无多线程竞争的场景下,完全消除同步操作的开销,让锁永久偏向于第一个获取它的线程。后续该线程重入同步块时,无需任何CAS操作,仅需简单的字段检查,性能几乎与无锁代码一致。
JDK17特殊说明
根据JEP 374规范,JDK15及以上版本默认禁用偏向锁,-XX:+UseBiasedLocking参数被标记为废弃,-XX:BiasedLockingStartupDelay默认值为0。若要开启偏向锁,需在JVM启动参数中添加:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 --add-opens java.base/java.lang=ALL-UNNAMED
其中--add-opens参数用于JOL工具查看对象头,适配JDK17的模块化机制。
偏向锁的获取流程
- 线程进入同步块时,检查对象
Mark Word的biased_lock=1、lock=01,确认处于可偏向状态。 - 检查
Mark Word中的线程ID字段:
- 若为当前线程ID,说明已持有偏向锁,直接进入同步块,无任何额外开销。
- 若为空,说明无线程持有偏向锁,当前线程通过CAS操作将自身线程ID写入
Mark Word。
- CAS成功:当前线程获得偏向锁,后续重入无需任何同步操作。
- CAS失败:说明有其他线程已竞争该锁,触发偏向锁撤销流程。
偏向锁的撤销机制
偏向锁的撤销是较重的操作,需要等待全局安全点(STW),暂停所有持有该锁的线程,检查持有偏向锁的线程状态:
- 若线程已死亡,直接将
Mark Word重置为无锁可偏向状态。 - 若线程仍存活,遍历该线程的栈帧,检查是否仍在使用该锁:
- 仍在使用:将偏向锁升级为轻量级锁,把
Displaced Mark Word写入线程的Lock Record,Mark Word改为指向Lock Record的指针,lock位改为00。 - 已不再使用:将
Mark Word重置为无锁可偏向状态,完成撤销。
批量重偏向与批量撤销
为了避免大量对象的偏向锁频繁撤销导致的STW开销,JVM引入了批量优化机制:
- 批量重偏向:当某个类的对象偏向锁撤销次数达到
-XX:BiasedLockingBulkRebiasThreshold(默认20),JVM会给该类的epoch字段加1,所有该类对象的偏向锁需重新偏向,解决同类型对象在多线程间交替使用导致的频繁撤销问题。 - 批量撤销:当某个类的对象偏向锁撤销次数达到
-XX:BiasedLockingBulkRevokeThreshold(默认40),JVM会判定该类存在持续的多线程竞争,直接禁用该类所有对象的偏向锁,后续创建的该类对象均为不可偏向状态。
3.3 第三阶段:轻量级锁
核心设计思想
在多线程交替使用锁、无同时竞争的场景下,避免重量级锁的用户态与内核态切换开销,基于线程栈帧中的Lock Record和CAS操作实现,属于用户态自旋锁,开销远小于重量级锁。
轻量级锁的获取流程
- 线程进入同步块时,若对象处于无锁不可偏向状态,或偏向锁撤销后需升级,JVM会在当前线程的栈帧中创建
Lock Record空间,用于存储对象当前Mark Word的拷贝,该拷贝称为Displaced Mark Word。 - 线程通过CAS操作,尝试将对象的
Mark Word替换为指向当前线程栈帧中Lock Record的指针。 - CAS成功:当前线程获得轻量级锁,将
Mark Word的lock位设置为00,执行同步代码。 - CAS失败:说明其他线程已持有该对象的轻量级锁,当前线程进入自适应自旋阶段,不断重试CAS操作尝试获取锁。
自适应自旋机制
JDK17中的自旋锁是完全自适应的,JVM会根据前一次在同一个锁上的自旋时间、锁持有者的状态,动态调整自旋次数:
- 若前一次自旋成功获取了锁,JVM会认为本次自旋大概率成功,增加自旋次数。
- 若前一次自旋失败,JVM会减少自旋次数,甚至直接跳过自旋升级为重量级锁,避免CPU空转浪费资源。
轻量级锁的释放流程
- 线程执行完同步代码后,通过CAS操作,将
Lock Record中存储的Displaced Mark Word写回对象的Mark Word。 - CAS成功:锁释放完成,对象回到无锁状态。
- CAS失败:说明自旋过程中锁已被升级为重量级锁,需进入重量级锁的释放流程,唤醒等待队列中的线程。
适用场景
轻量级锁适用于多线程交替执行同步块、锁持有时间短、无同时竞争的场景。若多个线程同时竞争锁,轻量级锁的自旋会消耗大量CPU资源,性能反而会急剧下降,此时JVM会将锁升级为重量级锁。
3.4 第四阶段:重量级锁
核心设计思想
在多线程同时激烈竞争锁的场景下,通过操作系统的互斥量(mutex)实现同步,避免CPU空转,保证线程安全。
核心载体:ObjectMonitor
重量级锁的底层是ObjectMonitor对象,其核心结构来自OpenJDK 17源码:
_owner:指向当前持有锁的线程。_EntryList:竞争锁失败的线程进入该队列,处于BLOCKED阻塞状态。_WaitSet:调用了wait()方法的线程进入该队列,处于WAITING等待状态。_count:锁的重入次数。_recursions:锁的重入深度。
重量级锁的获取流程
- 当轻量级锁的自适应自旋多次失败,CAS仍未成功,JVM会将对象的锁升级为重量级锁,初始化
ObjectMonitor对象,将对象的Mark Word设置为指向ObjectMonitor的指针,lock位改为10。 - 竞争锁的线程进入
ObjectMonitor的_EntryList队列,线程状态从RUNNABLE变为BLOCKED,操作系统将线程挂起,发生用户态到内核态的切换。 - 持有锁的线程释放锁时,会从
_EntryList中唤醒一个线程,重新竞争锁。 - 重入机制:持有锁的线程可多次重入,
_count和_recursions字段会记录重入次数,释放时需对应次数的monitorexit,直到_count为0,锁才会真正释放。
性能开销说明
重量级锁的核心开销来自用户态与内核态的切换,线程的阻塞和唤醒需要操作系统内核完成,涉及进程上下文的保存和恢复,开销极大。因此重量级锁适用于锁持有时间长、竞争激烈的场景,此时切换开销远小于CPU空转的开销。
四、锁升级全流程实战代码验证
以下所有代码均基于JDK17编写,用于验证上述锁升级的核心原理。
4.1 项目依赖配置(pom.xml)
采用所有组件的最新稳定版本,符合生产环境规范:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jam</groupId>
<artifactId>synchronized-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<lombok.version>1.18.34</lombok.version>
<jol.version>0.17</jol.version>
<slf4j.version>2.0.12</slf4j.version>
<logback.version>1.5.6</logback.version>
<spring.version>6.1.15</spring.version>
<fastjson2.version>2.0.52</fastjson2.version>
<guava.version>33.1.0-jre</guava.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>${jol.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.2 无锁状态与对象头验证
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
import org.springframework.util.ObjectUtils;
/**
* 无锁状态对象头验证示例
*
* @author ken
* @date 2026-03-05
*/
@Slf4j
public class NoLockDemo {
private static final Object LOCK_OBJECT = new Object();
public static void main(String[] args) {
log.info("===== 刚创建的对象,无锁状态 =====");
printObjectHeader(LOCK_OBJECT);
log.info("===== 调用identityHashCode()之后的对象头 =====");
int identityHashCode = System.identityHashCode(LOCK_OBJECT);
log.info("对象的identityHashCode: {}", Integer.toHexString(identityHashCode));
printObjectHeader(LOCK_OBJECT);
}
/**
* 打印对象头信息
*
* @param obj 待打印的对象
*/
private static void printObjectHeader(Object obj) {
if (ObjectUtils.isEmpty(obj)) {
log.error("打印对象头失败,对象为空");
return;
}
String classLayout = ClassLayout.parseInstance(obj).toPrintable();
log.info("\n{}", classLayout);
}
}
运行说明:需添加JVM参数--add-opens java.base/java.lang=ALL-UNNAMED,运行后可观察到:刚创建的对象Mark Word的偏向锁标志位,调用identityHashCode后,哈希值被写入Mark Word,偏向锁标志位变为0,对象进入不可偏向状态。
4.3 偏向锁状态验证
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
import org.springframework.util.ObjectUtils;
/**
* 偏向锁状态验证示例
* 需添加JVM启动参数:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 --add-opens java.base/java.lang=ALL-UNNAMED
*
* @author ken
* @date 2026-03-05
*/
@Slf4j
public class BiasedLockDemo {
private static final Object LOCK_OBJECT = new Object();
public static void main(String[] args) throws InterruptedException {
log.info("===== 刚创建的对象,可偏向无锁状态 =====");
printObjectHeader(LOCK_OBJECT);
log.info("===== 主线程第一次获取锁,偏向锁获取 =====");
synchronized (LOCK_OBJECT) {
printObjectHeader(LOCK_OBJECT);
}
log.info("===== 主线程释放锁之后,偏向锁状态保持 =====");
printObjectHeader(LOCK_OBJECT);
log.info("===== 主线程第二次重入锁,偏向锁重入 =====");
synchronized (LOCK_OBJECT) {
printObjectHeader(LOCK_OBJECT);
}
log.info("===== 新线程竞争锁,触发偏向锁撤销 =====");
Thread thread = new Thread(() -> {
synchronized (LOCK_OBJECT) {
log.info("===== 新线程获取锁,锁升级为轻量级锁 =====");
printObjectHeader(LOCK_OBJECT);
}
});
thread.start();
thread.join();
log.info("===== 偏向锁撤销后,对象最终状态 =====");
printObjectHeader(LOCK_OBJECT);
}
/**
* 打印对象头信息
*
* @param obj 待打印的对象
*/
private static void printObjectHeader(Object obj) {
if (ObjectUtils.isEmpty(obj)) {
log.error("打印对象头失败,对象为空");
return;
}
String classLayout = ClassLayout.parseInstance(obj).toPrintable();
log.info("\n{}", classLayout);
}
}
运行结果:开启偏向锁后,主线程第一次获取锁时,Mark Word中写入主线程ID,进入偏向锁状态;释放锁后偏向锁状态保持,重入无额外开销;新线程竞争锁触发偏向锁撤销,锁升级为轻量级锁,lock位变为00。
4.4 轻量级锁状态验证
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
import org.springframework.util.ObjectUtils;
/**
* 轻量级锁状态验证示例
* 需添加JVM启动参数:--add-opens java.base/java.lang=ALL-UNNAMED
*
* @author ken
* @date 2026-03-05
*/
@Slf4j
public class LightweightLockDemo {
private static final Object LOCK_OBJECT = new Object();
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
log.info("===== 初始无锁状态 =====");
printObjectHeader(LOCK_OBJECT);
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
synchronized (LOCK_OBJECT) {
count++;
log.info("线程1执行同步代码,当前count: {}", count);
if (i == 2) {
log.info("===== 线程1持有锁,轻量级锁状态 =====");
printObjectHeader(LOCK_OBJECT);
}
}
}
}, "thread-1");
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
synchronized (LOCK_OBJECT) {
count++;
log.info("线程2执行同步代码,当前count: {}", count);
if (i == 2) {
log.info("===== 线程2持有锁,轻量级锁状态 =====");
printObjectHeader(LOCK_OBJECT);
}
}
}
}, "thread-2");
thread1.start();
thread1.join();
thread2.start();
thread2.join();
log.info("===== 所有线程执行完毕,最终count: {}, 锁状态 =====", count);
printObjectHeader(LOCK_OBJECT);
}
/**
* 打印对象头信息
*
* @param obj 待打印的对象
*/
private static void printObjectHeader(Object obj) {
if (ObjectUtils.isEmpty(obj)) {
log.error("打印对象头失败,对象为空");
return;
}
String classLayout = ClassLayout.parseInstance(obj).toPrintable();
log.info("\n{}", classLayout);
}
}
运行结果:两个线程交替执行同步代码,无同时竞争,锁保持轻量级锁状态,lock位为00,Mark Word中存储指向持有锁线程栈帧中Lock Record的指针。
4.5 重量级锁状态验证
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
import org.springframework.util.ObjectUtils;
import java.util.concurrent.CountDownLatch;
/**
* 重量级锁状态验证示例
* 需添加JVM启动参数:--add-opens java.base/java.lang=ALL-UNNAMED
*
* @author ken
* @date 2026-03-05
*/
@Slf4j
public class HeavyweightLockDemo {
private static final Object LOCK_OBJECT = new Object();
private static int count = 0;
private static final int THREAD_COUNT = 10;
private static final int LOOP_COUNT = 1000;
public static void main(String[] args) throws InterruptedException {
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(THREAD_COUNT);
log.info("===== 初始无锁状态 =====");
printObjectHeader(LOCK_OBJECT);
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
try {
startLatch.await();
for (int j = 0; j < LOOP_COUNT; j++) {
synchronized (LOCK_OBJECT) {
count++;
if (count == 5000) {
log.info("===== 多线程激烈竞争,锁升级为重量级锁 =====");
printObjectHeader(LOCK_OBJECT);
}
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("线程执行异常", e);
} finally {
endLatch.countDown();
}
}, "thread-" + i).start();
}
startLatch.countDown();
endLatch.await();
log.info("===== 所有线程执行完毕,最终count: {}, 锁最终状态 =====", count);
printObjectHeader(LOCK_OBJECT);
}
/**
* 打印对象头信息
*
* @param obj 待打印的对象
*/
private static void printObjectHeader(Object obj) {
if (ObjectUtils.isEmpty(obj)) {
log.error("打印对象头失败,对象为空");
return;
}
String classLayout = ClassLayout.parseInstance(obj).toPrintable();
log.info("\n{}", classLayout);
}
}
运行结果:10个线程同时竞争锁,自适应自旋快速失败,锁升级为重量级锁,lock位变为10,Mark Word中存储指向ObjectMonitor对象的指针,最终count值为10000,保证线程安全。
4.6 identityHashCode导致偏向锁失效验证
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
import org.springframework.util.ObjectUtils;
/**
* 调用identityHashCode后偏向锁失效验证示例
* 需添加JVM启动参数:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 --add-opens java.base/java.lang=ALL-UNNAMED
*
* @author ken
* @date 2026-03-05
*/
@Slf4j
public class BiasedLockInvalidDemo {
private static final Object LOCK_OBJECT_1 = new Object();
private static final Object LOCK_OBJECT_2 = new Object();
public static void main(String[] args) {
log.info("===== 锁对象1:刚创建,可偏向状态 =====");
printObjectHeader(LOCK_OBJECT_1);
log.info("===== 锁对象1:获取锁,进入偏向锁状态 =====");
synchronized (LOCK_OBJECT_1) {
printObjectHeader(LOCK_OBJECT_1);
}
log.info("===== 锁对象2:刚创建,可偏向状态 =====");
printObjectHeader(LOCK_OBJECT_2);
log.info("===== 锁对象2:调用identityHashCode =====");
int identityHashCode = System.identityHashCode(LOCK_OBJECT_2);
log.info("锁对象2的identityHashCode: {}", Integer.toHexString(identityHashCode));
printObjectHeader(LOCK_OBJECT_2);
log.info("===== 锁对象2:获取锁,无法进入偏向锁,直接进入轻量级锁 =====");
synchronized (LOCK_OBJECT_2) {
printObjectHeader(LOCK_OBJECT_2);
}
}
/**
* 打印对象头信息
*
* @param obj 待打印的对象
*/
private static void printObjectHeader(Object obj) {
if (ObjectUtils.isEmpty(obj)) {
log.error("打印对象头失败,对象为空");
return;
}
String classLayout = ClassLayout.parseInstance(obj).toPrintable();
log.info("\n{}", classLayout);
}
}
运行结果:锁对象1正常进入偏向锁状态;锁对象2调用identityHashCode后,偏向锁标志位变为0,获取锁时直接进入轻量级锁状态,验证了位空间冲突导致偏向锁失效的核心原理。
五、核心细节与易混淆点辨析
5.1 锁升级是完全不可逆的吗?
这是最常见的认知错误。准确结论是:锁的膨胀过程在持有期间是单向的,只能从低级别升级到高级别,不可降级;但当锁完全释放后,对象的Mark Word会被重置为无锁状态,下一次竞争会重新开始锁升级流程。例如,一个对象的锁升级为重量级锁后,当所有线程都释放锁,对象会回到无锁状态,下一次线程获取锁时,会重新从偏向锁(开启时)或轻量级锁开始。
5.2 三种锁类型核心区别对比
| 锁类型 | 核心实现 | 适用场景 | 性能开销 | 线程安全保障 |
| 偏向锁 | Mark Word存储偏向线程ID,无CAS操作 | 单线程长期使用锁,无多线程竞争 | 几乎为0,与无锁代码一致 | 无竞争下线程安全 |
| 轻量级锁 | 线程栈帧Lock Record + 自适应CAS自旋 | 多线程交替使用锁,持有时间短,无同时竞争 | 用户态CAS操作,开销小,自旋失败消耗CPU | 交替竞争下线程安全 |
| 重量级锁 | 操作系统互斥量mutex + ObjectMonitor | 多线程同时激烈竞争,锁持有时间长 | 用户态与内核态切换,开销极大 | 激烈竞争下线程安全 |
5.3 轻量级锁和自旋锁的关系?
轻量级锁获取失败后会采用自旋重试,但自旋锁不等于轻量级锁。自旋是一种获取锁的重试机制,不仅轻量级锁会使用,重量级锁在进入阻塞之前也会尝试自旋。轻量级锁的核心是基于Lock Record的CAS操作,自旋是其竞争失败后的优化手段。
5.4 对象锁和类锁的核心区别?
- 对象锁:锁对象为实例对象,不同实例的对象锁互不影响,
synchronized修饰实例方法、synchronized(this)均为对象锁。 - 类锁:锁对象为类的
Class对象,一个类仅有一个Class对象,因此类锁对该类的所有实例生效,synchronized修饰静态方法、synchronized(XXX.class)均为类锁。 - 本质上,两种锁的底层锁升级机制完全一致,仅锁对象不同。
5.5 为什么wait/notify必须在synchronized同步块中调用?
这三个方法均基于ObjectMonitor实现:调用wait()会释放锁并将线程加入WaitSet队列,调用notify()会唤醒WaitSet中的线程,这些操作都必须先持有对象的监视器锁。若不在同步块中调用,JVM会抛出IllegalMonitorStateException异常,这是JVM规范的强制要求,也是为了保证线程间通信的原子性和线程安全。
5.6 JDK17与JDK8中synchronized的核心区别?
- 偏向锁:JDK8默认开启偏向锁,JDK17默认禁用偏向锁,相关参数被标记为废弃。
- 自旋优化:JDK17的自适应自旋算法更智能,能根据运行时情况动态调整自旋次数,减少CPU资源浪费。
- 内存屏障:JDK17对synchronized的内存屏障语义做了优化,更贴合JMM规范,多核CPU下性能表现更好。
- 偏向锁优化:JDK17优化了批量重偏向/撤销机制,减少了STW时间。
六、生产环境最佳实践
6.1 锁粒度最小化原则
在保证线程安全的前提下,尽量缩小锁的范围,仅对需要同步的共享变量操作加锁,不要对整个方法加锁,减少锁的持有时间,降低竞争概率,避免锁快速升级为重量级锁。
- 反例:整个方法加
synchronized,内部包含大量非线程安全的耗时IO操作。 - 正例:仅对共享变量的修改操作加锁,耗时操作移出同步块。
6.2 避免在同步块中执行耗时操作
同步块中若包含IO操作、网络调用、数据库查询等耗时操作,会导致锁持有时间大幅增加,多线程竞争概率急剧上升,锁会快速升级为重量级锁,性能严重下降。若必须执行耗时操作,尽量将其移出同步块。
6.3 合理选择锁对象
- 锁对象必须是
private final修饰的自定义对象,保证锁的封装性,避免外部代码获取锁对象导致死锁。 - 禁止用
String、Integer等包装类型作为锁对象,常量池缓存会导致锁对象被复用,引发非预期的线程安全问题。 - 禁止用
this作为锁对象,外部代码可获取到该实例,存在死锁风险。
6.4 JVM参数优化建议
- 高并发激烈竞争场景:保持JDK17默认配置,关闭偏向锁,避免偏向锁频繁撤销带来的STW开销。
- 单线程低竞争场景:可开启偏向锁,减少同步开销,提升性能。
- 自旋次数:不建议手动调整自旋次数,JDK17的自适应自旋已高度优化,手动调整反而可能导致性能下降。
6.5 利用JIT的锁优化机制
JIT编译器会对synchronized做两项核心优化,可通过合理编码提升优化效果:
- 锁消除:JIT通过逃逸分析,发现对象不会逃逸出当前线程,会直接消除该对象的同步锁。尽量减少对象逃逸,提升锁消除概率。
- 锁粗化:JIT发现相邻的多个同步块使用同一个锁对象,会将其合并为一个同步块,减少多次加锁解锁的开销。避免在循环中频繁加锁解锁,尽量将同步块放在循环外部。
6.6 死锁规避规范
- 固定加锁顺序:所有线程按照统一的顺序获取锁,避免循环等待。
- 避免锁嵌套:尽量不要在一个同步块中获取另一个锁,减少死锁概率。
- 超时控制:若使用可重入锁,可设置
tryLock超时时间,避免无限期等待。
七、高频面试题与标准答案
- 简述synchronized的锁升级全流程答:synchronized的锁升级是JVM为减少同步开销做的自适应优化,基于对象的
Mark Word实现,流程如下:
- 无锁状态:对象刚创建,未被任何线程锁定,分为可偏向和不可偏向两种子状态。
- 偏向锁:开启偏向锁的前提下,第一个线程获取锁时,通过CAS将自身线程ID写入
Mark Word,锁偏向于该线程,后续重入无需任何同步操作,开销极小。 - 轻量级锁:出现多线程交替竞争锁时,偏向锁被撤销,升级为轻量级锁。线程在栈帧中创建
Lock Record,通过CAS将对象的Mark Word替换为指向Lock Record的指针,获取成功则持有锁,失败则进入自适应自旋。 - 重量级锁:自旋多次失败,多线程同时激烈竞争锁时,锁升级为重量级锁,底层基于操作系统互斥量实现,竞争失败的线程会被阻塞,发生用户态到内核态的切换,开销极大。 锁的膨胀过程在持有期间是单向的,不可降级,锁完全释放后会重置为无锁状态。
- synchronized和ReentrantLock的核心区别答:两者都是可重入的独占锁,核心区别如下:
- 底层实现:synchronized是JVM层面的关键字,基于对象头和
ObjectMonitor实现;ReentrantLock是JDK层面的API,基于AQS抽象队列同步器实现。 - 灵活性:synchronized的加锁解锁是自动的,无法手动控制;ReentrantLock可手动控制加锁解锁,支持尝试加锁、超时加锁、可中断加锁,灵活性更高。
- 功能特性:ReentrantLock支持公平锁和非公平锁(默认非公平),synchronized仅支持非公平锁;ReentrantLock支持多个条件变量
Condition,synchronized仅支持一个wait/notify队列。 - 性能:低竞争场景下,synchronized的偏向锁、轻量级锁性能更好;高竞争场景下,ReentrantLock性能更稳定,JDK17中两者性能差距已极小。
- 异常处理:synchronized会在异常时自动释放锁,不会导致死锁;ReentrantLock必须在finally块中手动释放锁,否则会导致死锁。
- 为什么调用identityHashCode会导致对象无法进入偏向锁状态?答:64位JVM中,对象的
identityHashCode是31位,正好占用了偏向锁状态下存储偏向线程ID的54位空间的低31位。一旦调用identityHashCode,Mark Word中会写入31位哈希值,没有足够空间存储偏向线程ID,因此JVM会强制禁用该对象的偏向锁,后续该对象获取锁时会直接进入轻量级锁流程。 - 什么是锁消除和锁粗化?答:两者都是JIT编译器对synchronized的优化手段:
- 锁消除:JIT通过逃逸分析,判断某个锁对象不会逃逸出当前线程,不会被其他线程访问,就会直接消除该同步锁,避免不必要的加锁解锁开销。
- 锁粗化:JIT发现相邻的多个同步块使用同一个锁对象,会将这些同步块合并为一个大的同步块,减少多次加锁解锁的开销,避免循环中频繁加锁解锁导致的性能损耗。
- synchronized的可重入性是如何实现的?答:synchronized的可重入性基于底层的计数器实现:
- 偏向锁/轻量级锁:通过线程栈帧中的
Lock Record数量记录重入次数,每重入一次创建一个新的Lock Record,释放时依次移除,直到所有Lock Record都被移除,锁才真正释放。 - 重量级锁:通过
ObjectMonitor中的_count和_recursions字段记录重入次数,线程每重入一次,计数器加1,释放时减1,直到计数器为0,锁才真正释放。
结尾
synchronized是Java并发编程的基石,锁升级机制是其性能优化的核心。只有彻底理解其底层实现原理,才能写出高效、安全的并发代码,从根源上规避生产环境中的并发问题。