synchronized 详解
场景:周五下午,飞翔科技的会议室里烟雾缭绕。大翔把一份线上事故报告摔在桌上:"我们的银行账户转账系统,昨天同一个账户被并发转了两次 5000 块,结果余额只扣了一次!"
白歌(表情严肃):"典型的线程安全问题。两个线程同时读到相同的余额,各自扣款后写回,后写的覆盖了先写的——这就是丢失更新。"
小崔(挠头):"那我加个 synchronized 不就行了?"
Frank:"It's not that simple. synchronized is the foundation, but you need to understand lock escalation, reentrancy, and the difference between class lock and instance lock. And in JDK 8, biased locking is enabled by default — that changes the game."
朱璐(在笔记本上快速翻看):"synchronized 从 JDK 1.6 开始引入了偏向锁、轻量级锁、重量级锁的升级机制。JDK 8 里,偏向锁默认延迟 4 秒启动——你知道吗?"
大翔:"那你从头给我们讲一遍吧。从 Mark Word 到锁升级,一个都别漏。"
一、定义速查表
| 属性 | 说明 |
|---|---|
| synchronized | Java 内置的互斥同步机制,保证同一时刻只有一个线程执行被保护的代码块。可修饰方法(实例/静态)和代码块 |
| 监视器锁(Monitor) | 每个 Java 对象都关联一个 Monitor,线程通过获取 Monitor 所有权进入 synchronized 区域。底层对应 monitorenter / monitorexit 字节码指令 |
| 对象头(Object Header) | 每个 Java 对象在堆内存中的头部区域,包含 Mark Word 和 Klass Pointer。Mark Word 存储锁状态、GC 年龄、hashCode 等 |
| Mark Word | 对象头中的机器字,根据锁状态复用存储空间。32 位 JVM 占 32bit,64 位 JVM 占 64bit(JDK 8 默认使用 64 位 JVM) |
| 偏向锁(Biased Locking) | 锁偏向于第一个获取它的线程,该线程再次进入无需 CAS 操作。JDK 8 默认开启,启动延迟 4 秒(-XX:BiasedLockingStartupDelay=0 可关闭延迟) |
| 轻量级锁(Lightweight Locking) | 通过 CAS 自旋尝试获取锁,适用于锁竞争不激烈场景。在栈帧中创建 Lock Record,CAS 替换 Mark Word |
| 重量级锁(Heavyweight Locking) | 依赖操作系统互斥量(Mutex),未获取锁的线程阻塞。涉及用户态↔内核态切换,性能开销大 |
| 锁升级(Lock Escalation) | 偏向锁 → 轻量级锁 → 重量级锁,单向不可逆。JDK 8 默认策略:先偏向,遇竞争再升级 |
| 可重入(Reentrant) | 同一线程可以多次获取同一把锁,不会死锁。synchronized 天生可重入,Monitor 内部维护重入计数器 |
| 锁粗化(Lock Coarsening) | JIT 编译器将多个连续的加锁/解锁操作合并为一个大锁范围,减少加解锁开销 |
| 锁消除(Lock Elimination) | JIT 编译器判定某段代码不可能被多线程访问时,直接移除 synchronized,通过逃逸分析实现 |
| 类锁 vs 实例锁 | synchronized 修饰静态方法锁定类的 Class 对象;修饰实例方法锁定当前实例 this;修饰代码块可指定任意锁对象 |
二、Mermaid 图解
2.1 锁升级状态图(偏向锁 → 轻量级锁 → 重量级锁)
2.2 Monitor 进入/退出流程图
三、synchronized 的三种用法
| 用法 | 锁对象 | 示例 |
|---|---|---|
| 同步实例方法 | this(当前实例对象) | public synchronized void method() {} |
| 同步静态方法 | 类名.class(Class 对象) | public static synchronized void method() {} |
| 同步代码块 | 任意指定对象 | synchronized(lockObj) { ... } |
关键区别:
- 实例方法锁 → 同一实例上的所有 synchronized 实例方法互斥
- 静态方法锁 → 整个类的所有 synchronized 静态方法互斥(全局锁)
- 实例锁和类锁相互独立,互不影响
四、完整代码示例
示例 1:飞翔科技银行账户并发转账 —— synchronized 方法保护
场景:大翔的工资卡(账号 10001)余额为 8888.88f,大翔在 ATM 取款,同时小崔用网银转出。如果不加锁,并发操作会导致余额错误。
/**
* 飞翔科技 - 银行账户并发转账安全问题演示
*
* 场景:账号 10001,初始余额 8888.88 元
* 大翔到 ATM 取款,小崔用网银转账同时发生
*/
public class BankTransferDemo {
// === 不安全版本:无同步保护 ===
static class UnsafeAccount {
private String owner;
private float balance; // 余额
public UnsafeAccount(String owner, float balance) {
this.owner = owner;
this.balance = balance;
}
// ✗ 没有 synchronized — 多线程并发调用会丢失更新
public void withdraw(float amount) {
if (balance >= amount) {
// 模拟网络延迟
try { Thread.sleep(10); } catch (InterruptedException e) { }
balance -= amount;
System.out.printf("[%s] 取款 %.2f 成功, 余额: %.2f\n",
Thread.currentThread().getName(), amount, balance);
} else {
System.out.printf("[%s] 余额不足! 当前余额: %.2f\n",
Thread.currentThread().getName(), balance);
}
}
public float getBalance() { return balance; }
}
// === 安全版本:synchronized 实例方法 ===
static class SafeAccount {
private String owner;
private float balance;
public SafeAccount(String owner, float balance) {
this.owner = owner;
this.balance = balance;
}
// ✓ synchronized 实例方法 — 锁是当前对象 this
public synchronized void withdraw(float amount) {
if (balance >= amount) {
try { Thread.sleep(10); } catch (InterruptedException e) { }
balance -= amount;
System.out.printf("[%s] 取款 %.2f 成功, 余额: %.2f\n",
Thread.currentThread().getName(), amount, balance);
} else {
System.out.printf("[%s] 余额不足! 当前余额: %.2f\n",
Thread.currentThread().getName(), balance);
}
}
public synchronized float getBalance() { return balance; }
}
public static void main(String[] args) throws InterruptedException {
System.out.println("========== 飞翔科技银行账户安全测试 ==========\n");
// === 测试不安全版本 ===
System.out.println("--- 不安全版本(无 synchronized)---");
UnsafeAccount unsafeAcc = new UnsafeAccount("大翔工资卡(10001)", 8888.88f);
Thread u1 = new Thread(() -> {
for (int i = 0; i < 8; i++) unsafeAcc.withdraw(555.55f);
}, "大翔-ATM");
Thread u2 = new Thread(() -> {
for (int i = 0; i < 8; i++) unsafeAcc.withdraw(555.55f);
}, "小崔-网银");
u1.start(); u2.start();
u1.join(); u2.join();
System.out.printf(">>> 不安全版本最终余额: %.2f (期望: %.2f)\n\n",
unsafeAcc.getBalance(), 8888.88f - 16 * 555.55f);
// === 测试安全版本 ===
System.out.println("--- 安全版本(synchronized 方法)---");
SafeAccount safeAcc = new SafeAccount("大翔工资卡(10001)", 8888.88f);
Thread s1 = new Thread(() -> {
for (int i = 0; i < 8; i++) safeAcc.withdraw(555.55f);
}, "大翔-ATM");
Thread s2 = new Thread(() -> {
for (int i = 0; i < 8; i++) safeAcc.withdraw(555.55f);
}, "小崔-网银");
s1.start(); s2.start();
s1.join(); s2.join();
System.out.printf(">>> 安全版本最终余额: %.2f (期望: %.2f)\n",
safeAcc.getBalance(), 8888.88f - 16 * 555.55f);
}
}
控制台输出(不安全版本可能出现余额错误):
========== 飞翔科技银行账户安全测试 ==========
--- 不安全版本(无 synchronized)---
[大翔-ATM] 取款 555.55 成功, 余额: 8333.33
[小崔-网银] 取款 555.55 成功, 余额: 7777.78
[大翔-ATM] 取款 555.55 成功, 余额: 7222.23
[小崔-网银] 取款 555.55 成功, 余额: 6666.68
...(并发操作导致丢失更新)
[大翔-ATM] 取款 555.55 成功, 余额: 1111.10
[小崔-网银] 取款 555.55 成功, 余额: 1111.10 ← 两个线程都从 1111.10 扣了款!
>>> 不安全版本最终余额: 1111.10 (期望: 0.00) ← 丢失更新!
--- 安全版本(synchronized 方法)---
[大翔-ATM] 取款 555.55 成功, 余额: 8333.33
[大翔-ATM] 取款 555.55 成功, 余额: 7777.78
...(串行化执行,每次扣款正确)
[小崔-网银] 取款 555.55 成功, 余额: 555.55
[小崔-网银] 取款 555.55 成功, 余额: 0.00
>>> 安全版本最终余额: 0.00 (期望: 0.00) ← 正确!
示例 2:飞翔科技多窗口售票系统 —— synchronized 代码块精确控制锁范围
场景:飞翔科技团建,白歌负责统一购买 50 张火车票(单价 188.88 元),开启 3 个抢票窗口。使用 synchronized 代码块精确控制锁粒度。
import java.util.ArrayList;
import java.util.List;
/**
* 飞翔科技 - 火车票售票系统 synchronized 代码块应用
*
* 场景:50 张票,单价 188.88 元,3 个窗口并发售票
* 使用 synchronized 代码块精确定义临界区,锁外执行耗时操作
*/
public class TicketSystemDemo {
static class TicketCounter {
private int remaining;
private final float ticketPrice = 188.88f;
public TicketCounter(int total) {
this.remaining = total;
}
// 专用锁对象 — 比锁 this 更安全(避免外部代码误用 this 锁)
private final Object ticketLock = new Object();
/**
* 售票方法 — 使用 synchronized 代码块
* 只有操作共享变量 remaining 的代码在锁内
* 日志打印、票据生成等耗时操作在锁外执行
*/
public int sellTicket(String windowName, int requestCount) {
// === 非临界区:日志、校验等不需要同步 ===
System.out.printf("[%s] 申请购买 %d 张票 (单价 %.2f)...\n",
windowName, requestCount, ticketPrice);
int actualSold;
float totalPrice;
// === 临界区:只锁住操作共享变量的代码 ===
synchronized (ticketLock) {
if (remaining >= requestCount) {
remaining -= requestCount;
actualSold = requestCount;
totalPrice = actualSold * ticketPrice;
System.out.printf("[%s] ✓ 成功购得 %d 张,小计 %.2f 元,剩余 %d 张\n",
windowName, actualSold, totalPrice, remaining);
} else if (remaining > 0) {
actualSold = remaining;
totalPrice = actualSold * ticketPrice;
remaining = 0;
System.out.printf("[%s] ⚠ 票不足!仅购得 %d 张,小计 %.2f 元,已售罄\n",
windowName, actualSold, totalPrice);
} else {
actualSold = 0;
totalPrice = 0;
System.out.printf("[%s] ✗ 票已售罄!\n", windowName);
}
}
// === 非临界区:打印票据(耗时操作放锁外) ===
if (actualSold > 0) {
try { Thread.sleep(30); } catch (InterruptedException e) { }
System.out.printf("[%s] 票据打印完成: %d 张,合计 %.2f 元\n",
windowName, actualSold, totalPrice);
}
return actualSold;
}
public int getRemaining() {
synchronized (ticketLock) {
return remaining;
}
}
}
public static void main(String[] args) throws InterruptedException {
TicketCounter counter = new TicketCounter(50);
List<String> salesLog = new ArrayList<>();
// 窗口1:大翔 — 每次买 3 张
Thread w1 = new Thread(() -> {
int total = 0;
for (int i = 0; i < 8; i++) {
total += counter.sellTicket("窗口1-大翔(工号10001)", 3);
try { Thread.sleep(15); } catch (InterruptedException e) { }
}
salesLog.add("窗口1-大翔 共购: " + total + " 张");
});
// 窗口2:白歌 — 每次买 5 张
Thread w2 = new Thread(() -> {
int total = 0;
for (int i = 0; i < 6; i++) {
total += counter.sellTicket("窗口2-白歌(工号10002)", 5);
try { Thread.sleep(15); } catch (InterruptedException e) { }
}
salesLog.add("窗口2-白歌 共购: " + total + " 张");
});
// 窗口3:小崔 — 每次买 2 张
Thread w3 = new Thread(() -> {
int total = 0;
for (int i = 0; i < 10; i++) {
total += counter.sellTicket("窗口3-小崔(工号10003)", 2);
try { Thread.sleep(15); } catch (InterruptedException e) { }
}
salesLog.add("窗口3-小崔 共购: " + total + " 张");
});
w1.start(); w2.start(); w3.start();
w1.join(); w2.join(); w3.join();
System.out.println("\n========== 售票汇总 ==========");
for (String log : salesLog) System.out.println(log);
System.out.printf("剩余票数: %d\n", counter.getRemaining());
}
}
控制台输出:
[窗口1-大翔(工号10001)] 申请购买 3 张票 (单价 188.88)...
[窗口1-大翔(工号10001)] ✓ 成功购得 3 张,小计 566.64 元,剩余 47 张
[窗口2-白歌(工号10002)] 申请购买 5 张票 (单价 188.88)...
[窗口2-白歌(工号10002)] ✓ 成功购得 5 张,小计 944.40 元,剩余 42 张
[窗口3-小崔(工号10003)] 申请购买 2 张票 (单价 188.88)...
[窗口3-小崔(工号10003)] ✓ 成功购得 2 张,小计 377.76 元,剩余 40 张
...(并发售票过程)
[窗口1-大翔(工号10001)] ⚠ 票不足!仅购得 1 张,小计 188.88 元,已售罄
[窗口2-白歌(工号10002)] ✗ 票已售罄!
[窗口3-小崔(工号10003)] ✗ 票已售罄!
========== 售票汇总 ==========
窗口1-大翔 共购: 21 张
窗口2-白歌 共购: 25 张
窗口3-小崔 共购: 4 张
剩余票数: 0
五、可重入性详解
synchronized 是可重入锁:同一线程可以多次获取同一把锁,Monitor 内部维护一个计数器。
/**
* 飞翔科技 - synchronized 可重入性演示
*/
public class ReentrantDemo {
// 所有 synchronized 实例方法共享同一把 this 锁
public synchronized void methodA() {
System.out.println(Thread.currentThread().getName() + " 进入 methodA");
// methodA 内部调用 methodB — 同一线程,同一把锁,不会死锁
methodB(); // ✓ 可重入!
}
public synchronized void methodB() {
System.out.println(Thread.currentThread().getName() + " 进入 methodB");
methodC();
}
public synchronized void methodC() {
System.out.println(Thread.currentThread().getName() + " 进入 methodC");
}
public static void main(String[] args) {
ReentrantDemo demo = new ReentrantDemo();
// 线程 10001 号大翔多次进入锁
new Thread(() -> demo.methodA(), "大翔-10001").start();
}
}
控制台输出:
大翔-10001 进入 methodA
大翔-10001 进入 methodB
大翔-10001 进入 methodC
六、类锁 vs 实例锁
| 对比维度 | 实例锁(this) | 类锁(Class) |
|---|---|---|
| 修饰位置 | synchronized 实例方法 / synchronized(this){} | synchronized 静态方法 / synchronized(ClassName.class){} |
| 锁对象 | 当前实例对象 | 类的 Class 对象 |
| 作用范围 | 同一实例上的所有同步实例方法互斥 | 所有实例共享,全局互斥 |
| 不同实例 | 不同实例的同步方法可并行执行 | 无论多少个实例,静态同步方法都互斥 |
| 是否互斥 | 实例锁和类锁互不影响 | 实例锁和类锁互不影响 |
/**
* 飞翔科技 - 类锁与实例锁互不干扰演示
*/
public class ClassLockVsInstanceLock {
// 实例锁 — 锁定 this
public synchronized void instanceMethod() {
System.out.println(Thread.currentThread().getName() + " 获得实例锁");
try { Thread.sleep(2000); } catch (InterruptedException e) { }
System.out.println(Thread.currentThread().getName() + " 释放实例锁");
}
// 类锁 — 锁定 ClassLockVsInstanceLock.class
public static synchronized void classMethod() {
System.out.println(Thread.currentThread().getName() + " 获得类锁");
try { Thread.sleep(2000); } catch (InterruptedException e) { }
System.out.println(Thread.currentThread().getName() + " 释放类锁");
}
public static void main(String[] args) {
ClassLockVsInstanceLock obj = new ClassLockVsInstanceLock();
// t1 获取实例锁,t2 获取类锁 — 互不干扰,可同时执行
Thread t1 = new Thread(() -> obj.instanceMethod(), "大翔-10001");
Thread t2 = new Thread(() -> ClassLockVsInstanceLock.classMethod(), "白歌-10002");
long start = System.currentTimeMillis();
t1.start(); t2.start();
try { t1.join(); t2.join(); } catch (InterruptedException e) { }
long elapsed = System.currentTimeMillis() - start;
System.out.printf("总耗时: %d ms (如果互斥则需 4000ms,并行则约 2000ms)\n", elapsed);
}
}
七、JDK 8 偏向锁深入
JDK 8 中偏向锁默认开启,但有 4 秒启动延迟。延迟期间创建的对象直接进入轻量级锁。
/**
* 飞翔科技 - JDK 8 偏向锁延迟验证
* 运行时添加 JVM 参数查看锁状态:
* -XX:+PrintGCDetails -XX:+PrintFlagsFinal
* -XX:BiasedLockingStartupDelay=0 关闭延迟
*/
public class BiasedLockDemo {
public static void main(String[] args) throws InterruptedException {
// 延迟 6 秒后创建对象 — 此时偏向锁已激活
System.out.println("等待偏向锁激活...");
Thread.sleep(6000);
Object lock = new Object();
System.out.println("创建锁对象: " + lock);
// 查看对象头 — 可配合 jol-core 库查看 Mark Word
// System.out.println(ClassLayout.parseInstance(lock).toPrintable());
long start = System.currentTimeMillis();
// 线程 10001 号大翔反复进入同步块
for (int i = 0; i < 100000000; i++) {
synchronized (lock) {
// 偏向锁模式下,几乎无开销
}
}
long elapsed = System.currentTimeMillis() - start;
System.out.printf("100,000,000 次 synchronized 执行耗时: %d ms\n", elapsed);
}
}
八、易错场景
❌ 错误 1:用 String 常量做锁对象
// ✗ 错误:String 常量池中的对象是全局共享的
synchronized ("飞翔科技") { // 所有用 "飞翔科技" 加锁的代码都在抢同一把锁!
// ...
}
// ✓ 正确:使用专用 Object 或 final 成员变量
private final Object lock = new Object();
synchronized (lock) { ... }
❌ 错误 2:锁住可变对象
// ✗ 错误:Integer 在 -128~127 范围使用缓存,不同地方可能拿到同一对象
synchronized (new Integer(count)) { ... }
// ✗ 错误:锁对象被重新赋值,锁失效
private Object lock = new Object();
public void method() {
synchronized (lock) {
lock = new Object(); // 锁对象变了!其他线程可能拿到新对象
}
}
// ✓ 正确:锁对象使用 final 修饰
private final Object lock = new Object();
❌ 错误 3:synchronized 修饰 run() 方法
// ✗ 错误:run() 加了 synchronized,但每个线程是不同 Runnable 实例,锁不住
class MyTask implements Runnable {
public synchronized void run() { ... } // 锁的是不同实例,无效!
}
// ✓ 正确:共享同一个锁对象
public void run() {
synchronized (sharedLock) { ... }
}
❌ 错误 4:异常导致锁未释放
// synchronized 在异常时自动释放锁 — 这是正确的行为
// 但如果异常后的状态不一致(如转账扣了A的钱但抛异常没加给B),需要处理
public synchronized void transfer(Account from, Account to, float amount) {
from.withdraw(amount); // 扣款成功
// 如果这里抛异常...
to.deposit(amount); // 未执行!钱丢了!
}
// ✓ 正确:记录操作日志或回滚,确保数据一致性
✅ 正确示范:精确控制锁粒度
// ✓ 正确:只在操作共享数据时加锁,耗时的 IO/网络操作放锁外
public void processOrder(int orderId) {
// 锁外:查询数据库、网络请求等耗时操作
Order order = queryOrderFromDB(orderId);
synchronized (inventoryLock) {
// 锁内:只操作共享的商品库存
inventory.decrease(order.getProductId(), order.getQuantity());
}
// 锁外:发送通知、打印小票等
sendNotification(order.getUserId());
}
九、面试考点
Q1:synchronized 底层是如何实现的?
A:synchronized 基于 JVM 的 Monitor 机制实现。编译后,synchronized 代码块对应 monitorenter 和 monitorexit 两条字节码指令。monitorenter 尝试获取对象的 Monitor,成功则计数器 +1;monitorexit 将计数器 -1,归零时释放锁。synchronized 方法则通过方法表(method_info)中的 ACC_SYNCHRONIZED 标志位标识,JVM 调用时自动进行 monitorenter/monitorexit。
JDK 1.6 之后引入了偏向锁、轻量级锁、重量级锁的升级机制,极大优化了性能。偏向锁在 Mark Word 中记录线程 ID,无需 CAS;轻量级锁通过 CAS 自旋;重量级锁依赖 OS Mutex。
Q2:偏向锁、轻量级锁、重量级锁的区别和升级过程?
A:
| 锁状态 | 适用场景 | 实现机制 | 性能开销 |
|---|---|---|---|
| 偏向锁 | 只有一个线程访问同步块 | Mark Word 记录线程 ID,无需 CAS | 极低 |
| 轻量级锁 | 多线程交替访问,无实际竞争 | CAS 自旋 + Lock Record | 较低 |
| 重量级锁 | 多线程激烈竞争 | OS Mutex,线程阻塞/唤醒 | 高(用户态↔内核态切换) |
升级过程:偏向锁 →(有竞争)→ 轻量级锁 →(自旋超时或竞争加剧)→ 重量级锁。单向不可逆。
Q3:synchronized 和 ReentrantLock 的区别?
A:
| 对比维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 内置,Monitor 机制 | JDK API,基于 AQS |
| 锁释放 | 自动释放(代码块结束或异常) | 必须手动 unlock()(finally 中) |
| 可中断 | 不支持 | lockInterruptibly() 支持 |
| 超时获取 | 不支持 | tryLock(timeout, unit) 支持 |
| 公平锁 | 非公平 | 可选公平/非公平 |
| 条件变量 | wait()/notify() | 多个 Condition |
| 性能 | JDK 6+ 优化后接近 | 略优(尤其高并发) |
Q4:什么是可重入锁?synchronized 如何实现可重入?
A:可重入锁指同一线程可以多次获取同一把锁而不会死锁。synchronized 的可重入通过 Monitor 内部的重入计数器实现:每次 monitorenter 计数器 +1,monitorexit 计数器 -1,归零时真正释放锁。这使得一个 synchronized 方法可以调用另一个 synchronized 方法,而不会死锁。
大翔(合上笔记本):"synchronized 表面简单,底下全是学问。Mark Word、锁升级、可重入、Monitor——学完这个才算真正入门 Java 并发。"
白歌:"没错。但记住:synchronized 不是万能的。下一章我们看 volatile——轻量级的可见性保证。"
Frank:"And then Lock — when you need tryLock, interrupt, and fair lock, you must look beyond synchronized."