Java 锁:synchronized 深度解析
synchronized 是一种内置的 Java 关键字,用于实现线程同步。
当一个线程进入 synchronized 块或方法时,它会获得锁,而这把锁直接存储在 Java对象头中,从而阻止其他线程同时进入相同的 synchronized 块或方法,确保共享资源的互斥访问。
扩展:Java 对象头简述
以 Hotspot 虚拟机为例,对象头主要包含两部分:
- Mark Word:默认存储对象的 HashCode、分代年龄和锁标志位信息。它是一个非固定的数据结构,会根据对象的状态复用存储空间,运行期间存储的数据会随着锁标志位的变化而变化。
- Klass Pointer:对象指向它的类元数据的指针,虚拟机通过这个指针确定该对象属于哪个类的实例。
Java 对象头结构
一、synchronized 的三大特性
原子性
被 synchronized 修饰的同步块,里面的所有操作(读/写/计算)要么全部执行且执行完毕,要么完全不执行。它通过 monitorenter 和 monitorexit 字节码指令,保证了临界区代码不会被其他线程中途打断。
可见性
- 解锁(退出同步块)之前,必须将工作内存中的共享变量刷新回主内存
- 加锁(进入同步块)时,必须清空工作内存中该变量的副本,直接从主内存重新读取
所以一个线程修改了变量,其他线程立刻能看见最新值(这一点和 volatile 等价)。
有序性
synchronized 的有序性是指:在同一个锁的临界区内,代码虽然可能被指令重排,但单线程执行的结果不会变,且多线程之间因排队(串行化)而天然有序。
注意:它不禁止临界区内的指令重排(而
volatile直接禁止特定重排)。
二、synchronized 的使用方式
synchronized 可以修饰普通方法、静态方法和代码块。
核心保证:当
synchronized修饰一个方法或者一个代码块时,它能够保证在同一时刻最多只有一个线程执行该段代码。
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
重要特性:代码中没有显式的
unlock(),因为synchronized的锁释放是 JVM 自动完成 的(隐式释放),这是它相对于ReentrantLock最大的便捷性所在,也是 JVM 内置锁(Monitor)的核心设计。
2.1 修饰普通同步方法:锁 = 当前实例对象 this
对于普通同步方法,锁是当前实例对象(不同实例对象之间的锁互不影响)。
当在实例的非静态方法上使用 synchronized 关键字时,它会将该方法变成同步方法,相当于对当前实例对象(this)加锁,this 作为对象监视器。这意味着只有一个线程可以同时执行该实例方法,以确保对该实例的互斥访问。当前类会创建多个实例对象,synchronized 独立地控制每个实例对象的同步。
/**
* @className SyncInstanceMethod
* @description: 普通同步方法(锁是当前实例对象 this)
* @author legend
* @date 2026/8/13 13:18
* @version 1.0
*/
public class SyncInstanceMethod {
// 锁对象:当前实例对象 (this)
public synchronized void instanceMethod() {
System.out.println(Thread.currentThread().getName() + " 进入普通同步方法,锁对象: this = " + this);
try {
Thread.sleep(2000); // 模拟业务逻辑
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 离开普通同步方法");
}
// 测试代码
public static void main(String[] args) {
SyncInstanceMethod instance = new SyncInstanceMethod();
// 情况1:同一个实例,两个线程抢锁 -> 会互斥(A执行完B才能执行)
new Thread(instance::instanceMethod, "线程A").start();
new Thread(instance::instanceMethod, "线程B").start();
// 情况2:不同实例,各自执行 -> 不会互斥(并行执行)
SyncInstanceMethod instance1 = new SyncInstanceMethod();
SyncInstanceMethod instance2 = new SyncInstanceMethod();
new Thread(instance1::instanceMethod, "线程C").start();
new Thread(instance2::instanceMethod, "线程D").start();
}
}2.2 修饰静态同步方法:锁 = 当前类的 Class 对象
对于静态同步方法,锁是当前类的 Class 对象。
在静态方法上使用 synchronized 关键字时,它会将该方法变为同步静态方法,相当于对当前类的 Class 对象加锁,当前类的 Class 对象作为对象监视器。这意味着只有一个线程可以同时执行该静态方法,以确保对该类的互斥访问。
锁定的是类的 Class 对象,因此它会阻止不同实例以及静态方法之间的并发执行,因为它们共享相同的 Class 对象。
/**
* @className SyncStaticMethod
* @description: 静态同步方法 锁是当前类的 Class 对象
* @author legend
* @date 2026/8/13 13:22
* @version 1.0
*/
public class SyncStaticMethod {
// 锁对象:当前类的 Class 对象 (SyncStaticMethod.class)
public static synchronized void staticMethod() {
System.out.println(Thread.currentThread().getName() + " 进入静态同步方法,锁对象: Class = " + SyncStaticMethod.class );
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 离开静态同步方法");
}
// 测试代码
public static void main(String[] args) {
// 即使创建两个不同的实例,因为锁的是 SyncStaticMethod.class,所以依然会互斥
SyncStaticMethod instance1 = new SyncStaticMethod();
SyncStaticMethod instance2 = new SyncStaticMethod();
new Thread(() -> instance1.staticMethod(), "线程A").start();
new Thread(() -> instance2.staticMethod(), "线程B").start();
// 输出结果:线程A 和 线程B 绝对是一前一后执行
}
}2.3 修饰同步代码块:锁 = 括号中配置的对象
对于同步代码块,锁是 synchronized 括号里配置的对象。
可以使用 synchronized 关键字来创建同步代码块,这样可以指定要加锁的对象,括号中括起来的对象就是对象监视器。这允许更细粒度的控制,可以选择对某个特定对象进行同步,而不是整个方法或类。
/**
* @className SyncBlock
* @description: 同步方法块
* @author legend
* @date 2026/8/13 13:25
* @version 1.0
*/
public class SyncBlock {
// 自定义锁对象(可以是任何对象,通常用 private final 修饰)
private final Object customLock = new Object();
// 写法 A:锁括号里配置的是 this(效果等价于普通同步方法)
public void blockLockThis() {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " 锁住了 this 对象: " + this);
}
}
// 写法 B:锁括号里配置的是 类.class(效果等价于静态同步方法)
public void blockLockClass() {
synchronized (SyncBlock.class) {
System.out.println(Thread.currentThread().getName() + " 锁住了 Class 对象: " + SyncBlock.class);
}
}
// 写法 C:锁括号里配置的是自定义对象(细粒度锁,只锁特定资源)
public void blockLockCustom() {
synchronized (customLock) {
System.out.println(Thread.currentThread().getName() + " 锁住了自定义对象: " + customLock);
// 只有需要访问 customLock 保护资源的代码放在这里
}
}
// 测试代码
public static void main(String[] args) {
SyncBlock demo = new SyncBlock();
// 测试写法 C:因为锁的是 demo 内部的同一个 customLock,所以两个线程会互斥
new Thread(demo::blockLockCustom, "线程A").start();
new Thread(demo::blockLockCustom, "线程B").start();
}
}三、synchronized 实现原理
synchronized 底层原理是基于 JVM 的指令和对象的监视器(Monitor)来实现的。
核心流程:
- 当一个线程要执行一个被
synchronized修饰的方法或代码块时,它需要先获取该方法或代码块所属对象的监视器 - 如果获取成功,该线程就可以执行同步代码,并且监视器的计数器加一
- 如果获取失败,该线程就会阻塞,直到监视器被释放
- 当一个线程执行完同步代码后,它会释放监视器,并且监视器的计数器减一
- 如果计数器为零,说明没有线程持有该监视器,其他线程就可以竞争获取该监视器
3.1 字节码层面验证
我们可以使用 javap 命令来对字节码文件进行反编译验证:
javap -verbose out/production/synchornized-example/cn/legend/synclock/SyncInstanceMethod.class3.2 修饰方法:ACC_SYNCHRONIZED 标志
synchronized 修饰方法时,在字节码层面会有一个 ACC_SYNCHRONIZED 标志,用来表示该方法是同步的。
虚拟机可以从方法常量池中的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否被声明为同步方法(静态方法也是如此):
- 当方法调用时,调用指令将会检查方法的
ACC_SYNCHRONIZED访问标志是否被设置 - 如果设置了,执行线程就要求先成功持有管程(Monitor),然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程
- 在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程
- 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法边界之外时自动释放

3.3 修饰代码块:monitorenter / monitorexit 指令
synchronized 修饰代码块时,在字节码层面会有 monitorenter 和 monitorexit 两个指令,分别用来进入和退出监视器。
Java 虚拟机的指令集中有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义(monitorenter 和 monitorexit 两条指令是 C 语言的实现)。
正确实现 synchronized 关键字需要 Javac 编译器与 Java 虚拟机两者共同协作支持。Monitor 的实现基本都是 C++ 代码,通过 JNI(Java Native Interface)的操作,直接与 CPU 交互编程。

3.4 原理总结
synchronized 的底层实现原理可以概括为以下几点:
synchronized通过监视器锁来实现线程同步- 每个 Java 对象都有一个监视器锁
- 线程在获取了对象的监视器锁后,可以执行被修饰的代码
- 线程在释放了对象的监视器锁后,其他线程可以尝试获取监视器锁
四、synchronized 的四种锁状态(锁升级)
synchronized 在 JVM 中并不是一开始就是"重量级锁",而是会随着竞争情况逐步升级。
锁的状态从低到高依次为:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
4.1 无锁
无锁状态下没有对资源进行锁定,所有线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
无锁的特点是修改操作在循环内进行,线程会不断地尝试修改共享资源:如果没有冲突就修改成功并退出;否则继续循环重试(CAS 就是典型的无锁实现)。如果有多个线程修改同一个值,最终必定有一个线程修改成功,其他失败的线程会不断重试直到成功。
4.2 偏向锁
偏向锁是 JVM 在多线程环境下优化同步性能的一种锁机制,适用于大多数时间只有一个线程访问同步代码块的场景。
当一个线程第一次访问同步代码块时,JVM 会把锁"偏向"这个线程——在对象头中记录该线程 ID。后续该线程再次进入和退出同步代码块时,无需再做任何同步操作,从而大大降低了获取锁和释放锁的开销。
一旦有第二个线程尝试获取锁,偏向锁就会被撤销,升级为轻量级锁。
4.3 轻量级锁
轻量级锁是 JVM 实现的同步机制,主要用于锁竞争不激烈、线程持有锁时间较短的场景。它不像传统的重量级锁那样每次获取/释放都需要操作系统级别的互斥操作,而是尽量在用户态完成,避免频繁的线程阻塞和唤醒带来的开销。
工作原理:当偏向锁不再适用(锁的竞争不再局限于单个线程)时,JVM 会将锁升级为轻量级锁。线程通过自旋(Spin-Wait)的方式在一段时间内忙等锁的释放,而不是立即挂起线程——这样在锁竞争不激烈时,能够快速获得锁,提高响应速度和并发性能。
适用场景:同步代码块执行速度快、线程持有锁时间短、锁竞争不激烈的场景(如短期内只有一个或少数几个线程竞争同一共享资源)。
注意:
java.util.concurrent.locks.ReentrantLock并不等同于 JVM 层面的轻量级锁,但它的默认非公平模式在竞争不激烈时也会先通过自旋等轻量方式尝试获取;JVM 内置的synchronized在 JDK 1.6 之后引入了完整的锁升级机制,包含了偏向锁和轻量级锁的优化。
4.4 重量级锁
重量级锁是一种较为传统的互斥同步机制,通常涉及到操作系统的互斥量(Mutex)或监视器锁(Monitor),每次加锁/解锁都需要在用户态和内核态之间切换,开销较大。
在 Java 中,synchronized 关键字实现的锁在竞争激烈时会升级为重量级锁,它在 JVM 内部由 Monitor 对象实现,属于内建锁机制。另外,java.util.concurrent.locks 包下的 ReentrantLock 等显式锁在真正阻塞线程时也会进入类似重量级的阻塞状态(底层依赖操作系统的 park/unpark 或互斥量)。
核心作用:
- 确保任何时刻只有一个线程能够访问被锁定的资源或代码块,防止数据竞争和不一致
- 保证线程间的协同工作,确保在并发环境下执行的线程按照预定的顺序或条件进行操作
