本文目录导读:

- 目录导读
- 引言:为什么多线程需要同步控制?
- 案例背景:一个危险的银行账户取款程序
- 无锁版本演示:线程安全陷阱与数据错乱
- synchronized介入:三种经典加锁方式对比
- 核心原理:对象头、Monitor与锁升级过程
- 常见误区问答
- 何时该用synchronized
用真实案例讲透synchronized的同步锁机制
目录导读
- 引言:为什么多线程需要同步控制?
- 案例背景:一个危险的银行账户取款程序
- 无锁版本演示:线程安全陷阱与数据错乱
- synchronized介入:三种经典加锁方式对比
- 核心原理:对象头、Monitor与锁升级过程
- 常见误区问答:synchronized与性能、死锁
- 何时该用synchronized
引言:为什么多线程需要同步控制?
在多线程编程中,当多个线程同时访问共享资源(如变量、对象、文件)时,会产生“竞争条件”(Race Condition),例如两个线程同时给同一个银行账户取款,如果没有同步机制,最终余额可能比预期少扣或多扣。synchronized正是Java提供的最基础的线程同步关键字,它能保证同一时刻只有一个线程执行被修饰的代码块或方法。
案例背景:一个危险的银行账户取款程序
我们设计一个简单的银行账户类 BankAccount,包含余额 balance 和取款方法 withdraw(int amount),在主程序中创建两个线程,各执行100次取款1元的操作,期望最终余额减少200元,但如果不加同步,结果会怎样?
无锁版本演示:线程安全陷阱与数据错乱
核心代码(无synchronized):
class BankAccount {
private int balance = 200;
public void withdraw(int amount) {
// 非原子操作:读-减-写
if (balance >= amount) {
// 模拟耗时操作
Thread.sleep(1);
balance -= amount;
}
}
}
运行结果:多次执行后,余额可能是198、199、200等错误值,从未正确减少到0。
原因:balance >= amount 判断和 balance -= amount 之间被线程切换打断,导致多个线程都通过了检查,却只扣除了一次金额。
此案例清晰展示了临界区问题:多个线程并发执行“读取-修改-写入”操作时,必须互斥访问。
synchronized介入:三种经典加锁方式对比
同步代码块(锁当前对象)
public void withdraw(int amount) {
synchronized(this) {
if (balance >= amount) {
Thread.sleep(1);
balance -= amount;
}
}
}
- 效果:只有持有
this对象锁的线程才能进入代码块,其他线程阻塞等待。 - 适用:只保护部分代码,减少锁范围。
同步方法
public synchronized void withdraw(int amount) {
if (balance >= amount) {
Thread.sleep(1);
balance -= amount;
}
}
- 等价于
synchronized(this),锁住整个方法。 - 注意:如果继承类重写此方法,需考虑锁对象改变。
锁静态方法(类锁)
public static synchronized void staticWithdraw() { ... }
- 锁对象是
BankAccount.class,用于保护静态变量。
验证结果:添加任意一种synchronized后,无论执行多少次,余额始终准确递减到0,证明同步生效。
核心原理:对象头、Monitor与锁升级过程
每个Java对象都有一个对象头(Mark Word),其中存储锁状态信息:
- 无锁:对象头标记为001。
- 偏向锁:单线程反复获取同一锁时,将线程ID写入对象头,避免CAS开销。
- 轻量级锁:多线程轻度竞争时,通过自旋(Spin)等待,不挂起线程。
- 重量级锁:竞争激烈时,升级为Monitor(内部包含等待队列),线程进入阻塞。
synchronized会自动完成锁升级,从偏向锁→轻量级锁→重量级锁,随着竞争加剧而加重代价,这也是为什么synchronized比早期版本性能好很多的原因。
常见误区问答
Q1:synchronized能保证可见性吗?
A:能,进入synchronized块时会刷新工作内存,退出时会强制将修改写入主内存,因此具备原子性和可见性。
Q2:synchronized会引发死锁吗?
A:会,例如线程A持有锁1,等待锁2;线程B持有锁2,等待锁1,解决方案:按固定顺序获取锁,或使用tryLock等工具类。
Q3:synchronized和Lock接口有什么区别?
A:synchronized是关键字,使用简单,自动释放锁;Lock是接口,提供更灵活的控制(如可中断、超时、公平性),性能上两者在JDK 6以后差距很小。
Q4:为什么我的synchronized代码块没生效?
A:最常见原因是锁的不是同一个对象,例如用字符串常量"lock"作为锁,多个new String("lock")是不同对象,应使用同一个静态对象或Class对象。
何时该用synchronized
- 简单互斥场景:只需保护一个变量或一段代码,且不涉及超时、中断等高级需求。
- 代码可读性优先:相比
Lock代码更简洁,不易出错。 - 与JVM优化配合:现代JVM对
synchronized做了大量优化(锁消除、锁粗化、偏向锁),性能在大多数场景下足够。
本文通过银行取款案例,从现象到原理,从错误到正确,完整展示了synchronized如何解决多线程并发问题。 理解其本质,是掌握Java并发编程的关键一步。