synchronized关键字深度解析 ------ 从对象锁到锁升级
文章目录
- [synchronized关键字深度解析 ------ 从对象锁到锁升级](#synchronized关键字深度解析 —— 从对象锁到锁升级)
-
- 前言
- 一、线程安全问题回顾
-
- [1.1 并发问题的经典场景](#1.1 并发问题的经典场景)
- [1.2 synchronized的解决方案](#1.2 synchronized的解决方案)
- 二、synchronized的三种使用方式
-
- [2.1 修饰实例方法](#2.1 修饰实例方法)
- [2.2 修饰静态方法](#2.2 修饰静态方法)
- [2.3 修饰代码块](#2.3 修饰代码块)
- 锁定对象与本身方法锁互斥的陷阱
- 三、synchronized的底层原理
-
- [3.1 对象头中的Mark Word](#3.1 对象头中的Mark Word)
- [3.2 Monitor机制(重量级锁)](#3.2 Monitor机制(重量级锁))
- [3.3 可重入性验证](#3.3 可重入性验证)
- 四、锁升级过程(锁膨胀)
-
- [4.1 偏向锁(Biased Locking)](#4.1 偏向锁(Biased Locking))
- [4.2 轻量级锁(Lightweight Locking)](#4.2 轻量级锁(Lightweight Locking))
- [4.3 自适应自旋锁](#4.3 自适应自旋锁)
- [4.4 锁消除](#4.4 锁消除)
- [4.5 锁粗化](#4.5 锁粗化)
- [五、synchronized vs Lock](#五、synchronized vs Lock)
- 六、经典案例:多线程卖票
- 总结
- [✅ 亮点总结](#✅ 亮点总结)
- 适用场景
- 扩展方向
前言
在Java并发编程中,synchronized关键字是解决线程安全问题最基础、最常用的手段。几乎所有Java面试都会问到它的底层原理、锁升级过程、以及与Lock的区别。
很多人对synchronized的认知停留在"给代码块加锁"的层面,但它的底层机制远比表面复杂。synchronized从JDK 1.0就存在了,但在JDK 1.6之前,它确实被称为"重量级锁"------每次加锁都需要操作系统级别的monitor操作,性能很差。JDK 1.6进行了"synchronized性能革命",引入了偏向锁、轻量级锁、自适应自旋、锁消除、锁粗化等一系列优化,使得synchronized在大多数场景下的性能已经不输于ReentrantLock。这也是为什么JDK 1.8的ConcurrentHashMap放弃了JDK 1.7的分段锁(Segment + ReentrantLock),转而使用synchronized + CAS的组合。
本文将带你深入理解synchronized的方方面面:三种使用方式、底层Monitor机制、Mark Word在对象头中的变化、以及从偏向锁到轻量级锁再到重量级锁的完整升级过程。
一、线程安全问题回顾
1.1 并发问题的经典场景
java
public class ThreadSafetyIssue {
private static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter++; // 非原子操作
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("期望值: 20000, 实际值: " + counter);
// 每次运行结果可能不同,典型输出:17650、18432等
}
}
counter++看似一行代码,实际是三个步骤:读取 → 加1 → 写回。多线程交替执行这三个步骤,导致结果不确定。
这种问题被称为竞态条件(Race Condition) ------多个线程同时访问共享数据,且至少有一个线程在修改数据,最终结果取决于线程执行的精确时序。这种bug的特点是非常难以复现和调试:你可能本地测试100次都正常,上了生产环境偶尔出现一次数据错误。这也是为什么理解线程安全原理比"能用synchronized"重要得多------你得知道什么场景下需要加锁、加在什么地方、加多大范围。
1.2 synchronized的解决方案
java
public class SynchronizedSolution {
private static int counter = 0;
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized (lock) {
counter++;
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized (lock) {
counter++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("结果: " + counter); // 始终是 20000
}
}
二、synchronized的三种使用方式
synchronized可以修饰三种目标:实例方法、静态方法、代码块 。理解"锁的是什么"是使用synchronized的第一课------很多人写出了加锁代码但线程安全问题依然存在,原因就是锁错了对象。
核心记忆法则:
- 修饰实例方法 → 锁是
this(当前实例对象),不同实例各锁各的,互不影响 - 修饰静态方法 → 锁是
类名.class(Class对象),所有实例共享同一把锁 - 修饰代码块 → 锁是括号中指定的任意对象,灵活但容易出错
2.1 修饰实例方法
锁住的是当前实例对象(this)。不同实例的锁互不影响。
java
public class SyncOnInstanceMethod {
private int count = 0;
// 等价于 synchronized(this) 包裹整个方法体
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
SyncOnInstanceMethod demo = new SyncOnInstanceMethod();
// 多个线程操作同一个对象
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) demo.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) demo.increment();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(demo.getCount()); // 20000
}
}
2.2 修饰静态方法
锁住的是类的Class对象。同一个类的所有实例共享同一把锁。
java
public class SyncOnStaticMethod {
private static int count = 0;
// 等价于 synchronized(SyncOnStaticMethod.class)
public static synchronized void increment() {
count++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) increment();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count); // 20000
}
}
2.3 修饰代码块
锁住的是指定的对象。这是最灵活的方式,可以精确控制锁的粒度。
java
public class SyncBlockDemo {
private final Object readLock = new Object();
private final Object writeLock = new Object();
private StringBuilder data = new StringBuilder();
public void read() {
synchronized (readLock) {
System.out.println(Thread.currentThread().getName()
+ " 读取数据: " + data);
}
}
public void write(String content) {
synchronized (writeLock) {
data.append(content);
System.out.println(Thread.currentThread().getName()
+ " 写入数据: " + content);
}
}
public static void main(String[] args) {
SyncBlockDemo demo = new SyncBlockDemo();
// 读和写使用不同的锁,互不影响,提高并发度
new Thread(() -> demo.read(), "读线程").start();
new Thread(() -> demo.write("Hello"), "写线程").start();
new Thread(() -> demo.read(), "读线程2").start();
}
}
锁定对象与本身方法锁互斥的陷阱
java
// 实例方法锁和锁this是同一把锁,会互斥
// 静态方法锁和锁XXXX.class是同一把锁,会互斥
// 实例锁和类锁是两把不同的锁,不会互斥
public class LockInteraction {
public synchronized void method1() {
System.out.println("实例方法锁");
}
public void method2() {
synchronized (this) {
System.out.println("锁this");
}
}
// method1和method2是同一把锁,会互斥
public static synchronized void staticMethod() {
System.out.println("静态方法锁");
}
// staticMethod和上面method1/method2是不同的锁,不互斥
}
三、synchronized的底层原理
3.1 对象头中的Mark Word
每个Java对象在JVM中都有一个对象头 ,其中Mark Word记录了锁状态、GC分代年龄、HashCode等信息。
Mark Word 在不同锁状态下的结构(64位JVM):
无锁状态: [unused:25][hashcode:31][unused:1][age:4][biased_lock:1][lock:2]
其中: lock=01 表示无锁
偏向锁: [thread:54][epoch:2][unused:1][age:4][biased_lock:1][lock:2]
其中: biased_lock=1, lock=01 表示偏向锁
轻量级锁: [ptr_to_lock_record:62][lock:2]
其中: lock=00 表示轻量级锁
重量级锁: [ptr_to_monitor:62][lock:2]
其中: lock=10 表示重量级锁
3.2 Monitor机制(重量级锁)
重量级锁 基于操作系统的Monitor(管程)机制实现,依赖于操作系统的Mutex Lock(互斥锁):
java
// synchronized底层会生成 monitorenter 和 monitorexit 指令
// 对应 C++ 层面的 ObjectMonitor 对象
// 伪代码表示:
// monitorenter: 尝试获取对象的monitor
// 如果monitor的计数器为0,获取成功,计数器+1
// 如果当前线程已经持有monitor,计数器再+1(可重入)
// 否则,线程阻塞,等待monitor被释放
// monitorexit: 释放monitor
// 计数器-1
// 如果减到0,唤醒等待的线程
3.3 可重入性验证
synchronized是可重入锁------同一线程可以多次获取同一把锁:
java
public class ReentrantDemo {
public synchronized void methodA() {
System.out.println("进入方法A");
methodB(); // 在持有锁的情况下再次加锁------可重入
System.out.println("离开方法A");
}
public synchronized void methodB() {
System.out.println("进入方法B");
// 嵌套加锁同样支持
synchronized (this) {
System.out.println("进入方法B的同步块");
}
}
public static void main(String[] args) {
new ReentrantDemo().methodA();
}
}
/* 输出:
进入方法A
进入方法B
进入方法B的同步块
离开方法A
*/
四、锁升级过程(锁膨胀)
JDK 1.6之后,synchronized做了重大优化,锁的状态会随着竞争情况自动升级。这是synchronized面试题的重中之重。整个升级过程体现了JVM对"大部分锁竞争不会发生"这一假设的利用------先以最低成本的锁(偏向锁)处理最乐观的情况,随着竞争加剧逐步升级为成本更高的锁。
锁升级的核心思想:先用最轻的锁,不行再加码。就像你去图书馆占座------如果只有你一个人去,贴张纸条(偏向锁)就够了;偶尔有人跟你抢,你站旁边等一下(轻量级锁自旋);天天有人跟你抢,就得去前台登记排队了(重量级锁)。
无锁 ──→ 偏向锁 ──→ 轻量级锁 ──→ 重量级锁
(逐渐升级,不可降级)
注意:锁升级是单向的、不可逆的。一旦升级到轻量级锁或重量级锁,即使后续竞争消失,也不会降回偏向锁。但有一个例外------重量级锁在GC的STW(Stop The World)阶段可能会被降级。
4.1 偏向锁(Biased Locking)
偏向锁 认为大多数情况下锁不仅不存在竞争,而且总是由同一个线程多次获取。当线程第一次获取锁时,Mark Word中记录偏向线程ID。此后该线程再次进入同步块时,只需检查Mark Word中的线程ID是否是自己,如果是则直接进入------不需要CAS操作,也不需要monitor。
注意:偏向锁并非"免费"的------如果锁确实存在多线程竞争,撤销偏向锁本身也需要开销(需要到达安全点,即STW)。这也解释了为什么JDK 15+默认关闭了偏向锁:在现代高并发应用中,锁竞争比过去更常见,偏向锁带来的收益不如其撤销开销。在实际项目中,如果你的应用确实存在大量单线程使用的锁,可以通过JVM参数手动开启。
java
public class BiasedLockDemo {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
// JVM默认偏向锁会延迟4秒开启
Thread.sleep(5000);
// 第一次获取锁,升级为偏向锁
synchronized (lock) {
System.out.println("线程1持有偏向锁");
System.out.println("Mark Word: " + ClassLayout.parseInstance(lock).toPrintable());
}
}
}
JVM参数控制:
-XX:+UseBiasedLocking:启用偏向锁(JDK 15后默认关闭)-XX:BiasedLockingStartupDelay=0:关闭偏向锁启动延迟
4.2 轻量级锁(Lightweight Locking)
当有第二个线程尝试获取偏向锁时,偏向锁会撤销升级为轻量级锁。轻量级锁采用**CAS(Compare And Swap)**自旋尝试获取锁:
java
public class LightweightLockDemo {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock) {
System.out.println("t1 获取锁(偏向锁)");
try { Thread.sleep(2000); } catch (InterruptedException e) { }
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("t2 获取锁(升级为轻量级锁)");
// t2在等待期间会自旋尝试,不会立即阻塞
}
});
t1.start();
Thread.sleep(100); // 确保t1先获取锁
t2.start();
t1.join();
t2.join();
}
}
轻量级锁的特点:
- 适用于线程交替执行同步块的场景
- 自旋会消耗CPU,但避免了线程切换的开销
- 如果自旋超过一定次数(默认10次,JDK 6改为自适应),升级为重量级锁
4.3 自适应自旋锁
JDK 6引入了自适应自旋,自旋时间不再固定,而是根据上次在同一锁上的自旋时间及锁持有者的状态动态调整:
java
// 自适应自旋策略:
// - 如果之前自旋成功获取过锁,JVM会让自旋时间更长
// - 如果之前自旋很少成功,JVM会减少自旋甚至直接阻塞
// 让JVM自己根据实际情况来决定,开发者无需手动调优
4.4 锁消除
JIT编译时,JVM通过逃逸分析判断某些锁不可能发生竞争,直接将其消除:
java
public class LockElimination {
// StringBuffer的append()方法加了synchronized
// 但sb对象不会逃逸出方法,JVM会消除锁
public String concat() {
StringBuffer sb = new StringBuffer();
sb.append("Hello ");
sb.append("World");
return sb.toString();
}
// 等效于使用 StringBuilder(线程不安全)------ 无锁
}
4.5 锁粗化
JVM会将连续的加锁解锁合并为范围更大的锁,减少加解锁次数:
java
public class LockCoarsening {
private final Object lock = new Object();
// 优化前:反复加锁解锁
public void beforeOptimize() {
for (int i = 0; i < 100; i++) {
synchronized (lock) {
// 小操作
}
}
}
// JVM自动粗化为:
public void afterOptimize() {
synchronized (lock) {
for (int i = 0; i < 100; i++) {
// 小操作
}
}
}
}
五、synchronized vs Lock
| 对比维度 | synchronized | Lock |
|---|---|---|
| 实现层面 | JVM关键字,C++ native | JDK纯Java实现 |
| 锁获取 | 隐式获取释放 | 显式手动 lock()/unlock() |
| 可中断 | 不支持 | 支持 lockInterruptibly() |
| 超时获取 | 不支持 | 支持 tryLock(timeout) |
| 公平性 | 非公平 | 可选择公平/非公平 |
| 多条件 | 单个 (wait/notify) | 多个Condition |
| 性能 | JDK 6后差距很小 | 同 |
| 易用性 | 简单 | 需手动释放 |
六、经典案例:多线程卖票
java
public class TicketSelling {
private int tickets = 100;
public synchronized boolean sellTicket() {
if (tickets > 0) {
// 模拟出票耗时
try { Thread.sleep(10); } catch (InterruptedException e) { }
System.out.println(Thread.currentThread().getName()
+ " 售出第 " + (tickets--) + " 张票");
return true;
}
return false;
}
public static void main(String[] args) {
TicketSelling station = new TicketSelling();
for (int i = 1; i <= 4; i++) {
final String windowName = "窗口" + i;
new Thread(() -> {
while (station.sellTicket()) {
// 持续卖票直到售罄
}
}, windowName).start();
}
}
}
总结
synchronized从JDK 1.6开始已经不再是那个"重量级"的代名词。偏向锁、轻量级锁、自适应自旋、锁粗化、锁消除等一系列优化,让synchronized的性能在许多场景下已经不输于ReentrantLock。
理解锁升级的完整过程(无锁 → 偏向锁 → 轻量级锁 → 重量级锁)是面试中的核心考点。记住这个口诀:单线程用偏向锁,交替执行用轻量级锁,激烈竞争才用重量级锁。JVM通过这些优化策略,在保证线程安全的同时,尽可能地减少了锁带来的性能开销。
核心知识回顾:
- 三种使用方式:实例方法锁(锁this)、静态方法锁(锁Class对象)、代码块锁(锁指定对象)。实例锁和类锁互不影响------这是容易混淆的设计点。
- Monitor机制 :每个对象都有一个关联的Monitor对象,synchronized的加锁解锁底层就是
monitorenter/monitorexit指令。Monitor内部有计数器实现可重入性。 - Mark Word:对象头中的核心字段,不同锁状态对应不同的bit位结构。了解Mark Word是理解锁升级的前提。
- 锁优化:自适应自旋(根据历史决定自旋时长)、锁消除(逃逸分析)、锁粗化(合并连续加锁)、偏向锁延迟(默认4秒后开启)
最后提醒:在JDK 1.8及之后的版本中,对于大多数场景,首选synchronized------它语法简洁、自动释放(不会忘记unlock)、JVM持续优化。只有在需要可中断的锁获取 (lockInterruptibly)、超时尝试 (tryLock)或公平锁等特殊需求时,才使用ReentrantLock。
✅ 亮点总结
- synchronized的三种使用方式(实例方法/静态方法/代码块)及各自锁对象的精确辨析,实例锁与类锁互不影响
- Mark Word在无锁、偏向锁、轻量级锁、重量级锁四种状态下的64位结构变化图解
- 锁升级全链路机制:单线程偏向锁→交替执行轻量级锁(CAS自旋)→激烈竞争重量级锁(monitor/Mutex)
- JVM的五种锁优化策略:自适应自旋、锁消除(逃逸分析)、锁粗化、偏向锁延迟、轻量级锁CAS
- synchronized与Lock的全面对比:可中断性、超时获取、公平性、多条件队列等方面的差异
适用场景
- 多线程售票系统、库存扣减等需要原子性保护的经典并发场景
- 单例模式的线程安全实现(懒汉式synchronized方法或DCL + volatile)
- 对代码侵入性要求低、追求简洁可靠性的业务同步场景
扩展方向
- 深入学习ReentrantLock的高级特性(可中断lockInterruptibly()、超时tryLock()、公平锁)
- 研究JUC原子类(AtomicInteger、LongAdder)的无锁CAS并发方案
- 推荐阅读:18_Java中的Lock锁机制
下一篇:18_Java中的Lock锁机制