一、选手登场!🎬
🔵 蓝方:synchronized(老牌选手)
java
// synchronized:Java自带的语法糖
public synchronized void method() {
// 临界区代码
}
// 或者
public void method() {
synchronized(this) {
// 临界区代码
}
}
特点:
- 📜 JDK 1.0就有了,资历老
- 🎯 简单粗暴,写法简单
- 🤖 JVM级别实现,自动释放
- 💰 免费午餐,不需要手动管理
🔴 红方:ReentrantLock(新锐选手)
java
// ReentrantLock:JDK 1.5引入
ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock(); // 手动加锁
try {
// 临界区代码
} finally {
lock.unlock(); // 必须手动释放!
}
}
特点:
- 🆕 JDK 1.5新秀,年轻有活力
- 🎨 功能丰富,花样多
- 🏗️ API级别实现,灵活强大
- ⚠️ 需要手动管理,容易忘记释放
二、底层实现对决 💻
Round 1: synchronized的底层实现
1️⃣ 对象头结构(Mark Word)
scss
Java对象内存布局:
┌────────────────────────────────────┐
│ 对象头 (Object Header) │
│ ┌─────────────────────────────┐ │
│ │ Mark Word (8字节) │ ← 存储锁信息
│ ├─────────────────────────────┤ │
│ │ 类型指针 (4/8字节) │ │
│ └─────────────────────────────┘ │
├────────────────────────────────────┤
│ 实例数据 (Instance Data) │
├────────────────────────────────────┤
│ 对齐填充 (Padding) │
└────────────────────────────────────┘
Mark Word在不同锁状态下的变化:
scss
64位虚拟机的Mark Word(8字节=64位)
┌──────────────────────────────────────────────────┐
│ 无锁状态 (001) │
│ ┌────────────┬─────┬──┬──┬──┐ │
│ │ hashcode │ age │0 │01│ 未锁定 │
│ └────────────┴─────┴──┴──┴──┘ │
├──────────────────────────────────────────────────┤
│ 偏向锁 (101) │
│ ┌────────────┬─────┬──┬──┬──┐ │
│ │ 线程ID │epoch│1 │01│ 偏向锁 │
│ └────────────┴─────┴──┴──┴──┘ │
├──────────────────────────────────────────────────┤
│ 轻量级锁 (00) │
│ ┌────────────────────────────┬──┐ │
│ │ 栈中锁记录指针 │00│ 轻量级锁 │
│ └────────────────────────────┴──┘ │
├──────────────────────────────────────────────────┤
│ 重量级锁 (10) │
│ ┌────────────────────────────┬──┐ │
│ │ Monitor对象指针 │10│ 重量级锁 │
│ └────────────────────────────┴──┘ │
└──────────────────────────────────────────────────┘
2️⃣ 锁升级过程(重点!)
markdown
锁升级路径
无锁状态 偏向锁 轻量级锁 重量级锁
│ │ │ │
│ 第一次访问 │ 有竞争 │ 竞争激烈 │
├──────────────→ ├──────────────→ ├──────────────→ │
│ │ │ │
│ │ CAS失败 │ 自旋失败 │
│ │ │ │
🚶 一个人 🚶 还是一个人 🚶🚶 两个人 🚶🚶🚶 一群人
走路 (偏向这个人) 抢着走 排队走
详细解释:
阶段1:无锁 → 偏向锁
java
// 第一次有线程访问synchronized块
Thread-1第一次进入:
1. 对象处于无锁状态
2. Thread-1通过CAS在Mark Word中记录自己的线程ID
3. 成功!升级为偏向锁,偏向Thread-1
4. 下次Thread-1再来,发现Mark Word里是自己的ID,直接进入!
(就像VIP通道,不用检查)✨
生活比喻:
你第一次去常去的咖啡店☕,店员记住了你的脸。
下次你来,店员一看是你,直接给你做你的老口味,不用问!
阶段2:偏向锁 → 轻量级锁
java
Thread-2也想进入:
1. 发现偏向锁偏向的是Thread-1
2. Thread-1已经退出了,撤销偏向锁
3. 升级为轻量级锁
4. Thread-2通过CAS在栈帧中创建Lock Record
5. CAS将对象头的Mark Word复制到Lock Record
6. CAS将对象头指向Lock Record
7. 成功!获取轻量级锁 🎉
生活比喻:
咖啡店来了第二个客人,店员发现需要排队系统了。
拿出号码牌,谁先抢到谁先点单(自旋CAS)🎫
阶段3:轻量级锁 → 重量级锁
java
Thread-3、Thread-4、Thread-5也来了:
1. 多个线程竞争,CAS自旋失败
2. 自旋一定次数后,升级为重量级锁
3. 没抢到的线程进入阻塞队列
4. 需要操作系统介入,线程挂起(park)😴
生活比喻:
咖啡店人太多了!需要叫号系统 + 座位等待区。
没叫到号的人坐下来等,不用一直站着抢(操作系统介入)🪑
3️⃣ 字节码层面
java
public synchronized void method() {
System.out.println("hello");
}
字节码:
yaml
public synchronized void method();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED ← 看这里!方法标记
Code:
stack=2, locals=1, args_size=1
0: getstatic #2
3: ldc #3
5: invokevirtual #4
8: return
同步块字节码:
java
public void method() {
synchronized(this) {
System.out.println("hello");
}
}
yaml
public void method();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter ← 进入monitor
4: getstatic #2
7: ldc #3
9: invokevirtual #4
12: aload_1
13: monitorexit ← 退出monitor
14: goto 22
17: astore_2
18: aload_1
19: monitorexit ← 异常时也要退出
20: aload_2
21: athrow
22: return
Round 2: ReentrantLock的底层实现
基于AQS(AbstractQueuedSynchronizer)实现:
java
// ReentrantLock内部
public class ReentrantLock {
private final Sync sync;
// 抽象同步器
abstract static class Sync extends AbstractQueuedSynchronizer {
// ...
}
// 非公平锁实现
static final class NonfairSync extends Sync {
final void lock() {
// 先CAS抢一次
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 进入AQS队列
}
}
// 公平锁实现
static final class FairSync extends Sync {
final void lock() {
acquire(1); // 直接排队,不插队
}
}
}
数据结构:
ini
ReentrantLock
│
├─ Sync (继承AQS)
│ ├─ state: int (0=未锁,>0=重入次数)
│ └─ exclusiveOwnerThread: Thread (持锁线程)
│
└─ CLH队列
Head → Node1 → Node2 → Tail
↓ ↓
Thread2 Thread3
(等待) (等待)
三、功能对比大战 ⚔️
🏁 功能对比表
功能 | synchronized | ReentrantLock | 胜者 |
---|---|---|---|
加锁方式 | 自动 | 手动lock/unlock | synchronized ✅ |
释放方式 | 自动(异常也会释放) | 必须手动finally | synchronized ✅ |
公平锁 | 不支持 | 支持公平/非公平 | ReentrantLock ✅ |
可中断 | 不可中断 | lockInterruptibly() | ReentrantLock ✅ |
尝试加锁 | 不支持 | tryLock() | ReentrantLock ✅ |
超时加锁 | 不支持 | tryLock(timeout) | ReentrantLock ✅ |
Condition | 只有一个wait/notify | 可多个Condition | ReentrantLock ✅ |
性能(JDK6+) | 优化后差不多 | 差不多 | 平局 ⚖️ |
使用难度 | 简单 | 复杂,易出错 | synchronized ✅ |
锁信息 | 不易查看 | getQueueLength()等 | ReentrantLock ✅ |
🎯 详细功能对比
1️⃣ 可中断锁
java
// ❌ synchronized不可中断
Thread t = new Thread(() -> {
synchronized(lock) {
// 即使调用t.interrupt(),这里也不会响应
while(true) {
// 死循环
}
}
});
// ✅ ReentrantLock可中断
Thread t = new Thread(() -> {
try {
lock.lockInterruptibly(); // 可响应中断
// ...
} catch (InterruptedException e) {
System.out.println("被中断了!");
}
});
t.start();
Thread.sleep(100);
t.interrupt(); // 可以中断!
2️⃣ 尝试加锁
java
// ❌ synchronized没有tryLock
synchronized(lock) {
// 要么拿到锁,要么一直等
}
// ✅ ReentrantLock可以尝试
if (lock.tryLock()) { // 尝试获取,不阻塞
try {
// 拿到锁了
} finally {
lock.unlock();
}
} else {
// 没拿到,去做别的事
System.out.println("锁被占用,我去干别的");
}
// ✅ 还支持超时
if (lock.tryLock(3, TimeUnit.SECONDS)) { // 等3秒
try {
// 拿到了
} finally {
lock.unlock();
}
} else {
// 3秒还没拿到,放弃
System.out.println("等太久了,不等了");
}
3️⃣ 公平锁
java
// ❌ synchronized只能是非公平锁
synchronized(lock) {
// 后来的线程可能插队
}
// ✅ ReentrantLock可选公平/非公平
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁(默认)
公平锁 vs 非公平锁:
markdown
非公平锁(吞吐量高):
Thread-1持锁 → Thread-2排队 → Thread-3排队
↓
释放锁!
↓
Thread-4刚好来了,直接抢!(插队)✂️
虽然Thread-2先来,但Thread-4先抢到
公平锁(先来后到):
Thread-1持锁 → Thread-2排队 → Thread-3排队
↓
释放锁!
↓
Thread-4来了,但要排队到最后!
Thread-2先到先得 ✅
4️⃣ 多个条件变量
java
// ❌ synchronized只有一个等待队列
synchronized(lock) {
lock.wait(); // 只有一个等待队列
lock.notify(); // 随机唤醒一个
}
// ✅ ReentrantLock可以有多个Condition
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition(); // 条件1:未满
Condition notEmpty = lock.newCondition(); // 条件2:非空
// 生产者
lock.lock();
try {
while (queue.isFull()) {
notFull.await(); // 等待"未满"条件
}
queue.add(item);
notEmpty.signal(); // 唤醒"非空"条件的线程
} finally {
lock.unlock();
}
// 消费者
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 等待"非空"条件
}
queue.remove();
notFull.signal(); // 唤醒"未满"条件的线程
} finally {
lock.unlock();
}
四、性能对决 🏎️
JDK 1.5时代:ReentrantLock完胜
makefile
JDK 1.5性能测试(100万次加锁):
synchronized: 2850ms 😓
ReentrantLock: 1200ms 🚀
ReentrantLock快2倍多!
JDK 1.6之后:synchronized反击!
JDK 1.6对synchronized做了大量优化:
- ✅ 偏向锁(Biased Locking)
- ✅ 轻量级锁(Lightweight Locking)
- ✅ 自适应自旋(Adaptive Spinning)
- ✅ 锁粗化(Lock Coarsening)
- ✅ 锁消除(Lock Elimination)
makefile
JDK 1.8性能测试(100万次加锁):
synchronized: 1250ms 🚀
ReentrantLock: 1200ms 🚀
几乎一样了!
优化技术解析
1️⃣ 偏向锁
java
// 同一个线程反复进入
for (int i = 0; i < 1000000; i++) {
synchronized(obj) {
// 偏向锁:第一次CAS,后续直接进入
// 性能接近无锁!✨
}
}
2️⃣ 锁消除
java
public String concat(String s1, String s2) {
// StringBuffer是线程安全的,有synchronized
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
// JVM发现sb是局部变量,不可能有竞争
// 自动消除StringBuffer内部的synchronized!
// 性能大幅提升!🚀
3️⃣ 锁粗化
java
// ❌ 原代码:频繁加锁解锁
for (int i = 0; i < 1000; i++) {
synchronized(obj) {
// 很短的操作
}
}
// ✅ JVM优化后:锁粗化
synchronized(obj) { // 把锁提到循环外
for (int i = 0; i < 1000; i++) {
// 很短的操作
}
}
五、使用场景推荐 📝
优先使用synchronized的场景
1️⃣ 简单的同步场景
java
// 简单的计数器
private int count = 0;
public synchronized void increment() {
count++;
}
2️⃣ 方法级别的同步
java
public synchronized void method() {
// 整个方法同步,简单明了
}
3️⃣ 不需要高级功能
java
// 只需要基本的互斥,不需要tryLock、Condition等
synchronized(lock) {
// 业务代码
}
优先使用ReentrantLock的场景
1️⃣ 需要可中断的锁
java
// 可以响应中断,避免死锁
lock.lockInterruptibly();
2️⃣ 需要尝试加锁
java
// 拿不到锁就去做别的事
if (lock.tryLock()) {
// ...
}
3️⃣ 需要公平锁
java
// 严格按照先来后到
ReentrantLock fairLock = new ReentrantLock(true);
4️⃣ 需要多个条件变量
java
// 生产者-消费者模式
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
5️⃣ 需要获取锁的信息
java
// 查看等待的线程数
int waiting = lock.getQueueLength();
// 查看是否有线程在等待
boolean hasWaiters = lock.hasQueuedThreads();
六、常见坑点 ⚠️
坑1:ReentrantLock忘记unlock
java
// ❌ 危险!如果中间抛异常,永远不会释放锁
lock.lock();
doSomething(); // 可能抛异常
lock.unlock(); // 不会执行!💣
// ✅ 正确写法
lock.lock();
try {
doSomething();
} finally {
lock.unlock(); // 一定会执行
}
坑2:synchronized锁错对象
java
// ❌ 每次都是新对象,不起作用!
public void method() {
synchronized(new Object()) { // 💣 错误!
// 相当于没加锁
}
}
// ✅ 正确写法
private final Object lock = new Object();
public void method() {
synchronized(lock) {
// ...
}
}
坑3:锁的粒度太大
java
// ❌ 锁的范围太大,性能差
public synchronized void method() { // 整个方法都锁住
doA(); // 不需要同步
doB(); // 需要同步
doC(); // 不需要同步
}
// ✅ 缩小锁范围
public void method() {
doA();
synchronized(lock) {
doB(); // 只锁需要的部分
}
doC();
}
七、面试应答模板 🎤
面试官:synchronized和ReentrantLock有什么区别?
你的回答:
主要从实现层面和功能层面两个角度对比:
实现层面:
- synchronized是JVM层面的,基于monitor机制,通过对象头的Mark Word实现
- ReentrantLock是API层面的,基于AQS(AbstractQueuedSynchronizer)实现
功能层面,ReentrantLock更强大:
- 可中断:lockInterruptibly()可响应中断
- 可尝试:tryLock()非阻塞获取锁
- 可超时:tryLock(time)超时放弃
- 公平锁:可选择公平或非公平
- 多条件:支持多个Condition
- 可监控:可获取等待线程数等信息
性能对比:
- JDK 1.6之前ReentrantLock性能更好
- JDK 1.6之后synchronized做了大量优化(偏向锁、轻量级锁、锁消除、锁粗化),性能差不多
- synchronized优化包括:无锁→偏向锁→轻量级锁→重量级锁的升级路径
使用建议:
- 简单场景优先synchronized(代码简洁,自动释放)
- 需要高级功能时用ReentrantLock(可中断、超时、公平锁等)
举个例子: 如果只是简单的计数器,用synchronized即可。但如果是银行转账系统,需要可中断、可超时,就应该用ReentrantLock。
八、总结 🎯
scss
选择决策树:
需要同步?
│
Yes
│
┌─────────────┴─────────────┐
│ │
简单场景 复杂场景
(计数器、缓存等) (可中断、超时等)
│ │
synchronized ReentrantLock
│ │
✅ 简单 ✅ 功能强
✅ 自动释放 ⚠️ 需手动
✅ 性能好 ✅ 灵活
记忆口诀:
简单场景synchronized,
复杂需求ReentrantLock,
性能现在差不多,
根据场景来选择!🎵
最后一句话:
synchronized是"自动挡"🚗,简单好用;
ReentrantLock是"手动挡"🏎️,灵活强大!