Java中的synchronized与ReentrantLock
synchronized 与 ReentrantLock 全方位对比 — Java 并发锁的那些坑
🧑💻 作者:没用逆称
💡 前言:学并发编程的时候,最头疼的就是各种锁。synchronized 用了很久,后来发现 ReentrantLock 功能更强,但两者到底有什么区别?什么时候用哪个?面试问到也说不清楚。整理了一遍,写一篇笔记给自己总结一下。
一、为什么需要锁?先搞清楚问题
先看一个没有任何锁的情况:
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 这不是原子操作!
}
public int getCount() {
return count;
}
}
count++ 其实分三步:
- 读取 count 的值
- 加 1
- 写回 count
两个线程同时执行,count 的最终值可能小于预期。这就是竞态条件(Race Condition)。
线程1:读取 count=0 → 计算 0+1=1 → [被挂起]
线程2:读取 count=0 → 计算 0+1=1 → 写入 count=1
线程1:[恢复] → 写入 count=1 ← 覆盖了线程2的结果!
结论:多个线程操作共享变量,必须加锁。
二、synchronized — Java 内置锁
2.1 三种使用方式
// 方式1:修饰实例方法 → 锁的是当前对象(this)
public synchronized void method1() {
// 同一时刻只有一个线程能进入这个方法
}
// 方式2:修饰静态方法 → 锁的是 Class 对象(全局锁)
public static synchronized void method2() {
// 所有实例共享同一把锁
}
// 方式3:修饰代码块 → 锁的是括号里的对象(最灵活)
public void method3() {
synchronized (this) { // 锁当前对象
// 临界区代码
}
synchronized (MyClass.class) { // 锁 Class 对象
// 临界区代码
}
Object lock = new Object();
synchronized (lock) { // 锁自定义对象
// 临界区代码
}
}
2.2 基本特性
| 特性 | 说明 |
|---|---|
| 原子性 | 保证临界区代码原子执行 |
| 可见性 | unlock 前把工作内存写回主内存 |
| 有序性 | 禁止指令重排(同步块内的代码) |
| 可重入 | 同一线程可以多次获取同一把锁 |
| 不可中断 | 等待锁的线程无法被中断 |
2.3 可重入性演示
public class ReentrantDemo {
public synchronized void methodA() {
System.out.println("进入 methodA");
methodB(); // 同一线程,再次获取同一把锁 → 可重入
}
public synchronized void methodB() {
System.out.println("进入 methodB");
}
}
如果锁不可重入,上面的代码会死锁(自己等自己释放锁)。
三、ReentrantLock — 功能更强的显式锁
ReentrantLock 是 java.util.concurrent.locks 包下的显式锁,需要手动加锁和释放锁。
3.1 基本用法
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // 加锁
try {
count++;
} finally {
lock.unlock(); // 必须在 finally 里释放!否则死锁
}
}
}
⚠️ 最重要的一点:unlock() 必须放在 finally 块里,否则如果临界区抛异常,锁永远不释放,整个系统卡死。
3.2 公平锁 vs 非公平锁
ReentrantLock 构造函数可以传一个 fair 参数:
// 非公平锁(默认,性能更好)
ReentrantLock lock = new ReentrantLock();
// 或
ReentrantLock lock = new ReentrantLock(false);
// 公平锁(按等待顺序获取锁,吞吐量低)
ReentrantLock lock = new ReentrantLock(true);
| 模式 | 行为 | 优点 | 缺点 |
|---|---|---|---|
| 非公平锁(默认) | 新来的线程可以先抢锁 | 吞吐量高,减少线程切换 | 可能导致线程饥饿 |
| 公平锁 | 严格按等待队列顺序获取 | 每个线程都能获得锁 | 吞吐量低,频繁线程切换 |
💡 为什么默认用非公平锁? 非公平锁允许"插队",减少了线程挂起和唤醒的开销。在竞争激烈时,吞吐量通常比公平锁高很多。
四、ReentrantLock 的 4 大"杀手锏"功能
这是面试的重点:ReentrantLock 比 synchronized 强在哪里?
4.1 可中断等待(lockInterruptibly())
synchronized 等待锁时,线程无法被中断,只能一直等。
ReentrantLock 可以响应中断:
public void method() throws InterruptedException {
lock.lockInterruptibly(); // 可以被中断的获取锁
try {
// 临界区
} finally {
lock.unlock();
}
}
// 其他线程可以调用 thread.interrupt() 中断等待
使用场景:等待锁的时间太长,想超时放弃。
4.2 尝试获取锁(tryLock())
public void method() {
// 立即返回,获取不到锁不等待
if (lock.tryLock()) {
try {
// 获取到了锁
} finally {
lock.unlock();
}
} else {
// 没获取到锁,做其他事情
System.out.println("获取锁失败,不等待");
}
}
// 带超时的版本
public void method2() throws InterruptedException {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
// 2秒内获取到了锁
} finally {
lock.unlock();
}
} else {
// 2秒后还没获取到,放弃
System.out.println("等待超时,放弃");
}
}
synchronized 做不到这一点,它要么一直等,要么不进同步块,没有中间状态。
4.3 公平锁
上面已经讲过了,synchronized 是非公平锁,且不能改为公平模式。ReentrantLock 可以自由选择。
4.4 Condition — 比 wait()/notify() 更灵活
synchronized 配合 wait()/notify()/notifyAll() 实现线程间通信,但有个限制:只能有一个等待队列。
ReentrantLock 配合 Condition,可以创建多个等待队列:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedQueue<T> {
private final Object[] items;
private int count = 0;
private int takeIndex = 0;
private int putIndex = 0;
private final ReentrantLock lock = new ReentrantLock();
// 两个条件队列
private final Condition notEmpty = lock.newCondition(); // "队列非空" 条件
private final Condition notFull = lock.newCondition(); // "队列未满" 条件
public BoundedQueue(int capacity) {
this.items = new Object[capacity];
}
// 入队 — 队列满时等待
public void put(T item) throws InterruptedException {
lock.lock();
try {
// 队列满了,等待"队列未满"条件
while (count == items.length) {
notFull.await(); // 释放锁,等待被唤醒
}
items[putIndex] = item;
putIndex = (putIndex + 1) % items.length;
count++;
notEmpty.signal(); // 唤醒等待"队列非空"的线程
} finally {
lock.unlock();
}
}
// 出队 — 队列空时等待
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 释放锁,等待被唤醒
}
@SuppressWarnings("unchecked")
T item = (T) items[takeIndex];
items[takeIndex] = null;
takeIndex = (takeIndex + 1) % items.length;
count--;
notFull.signal(); // 唤醒等待"队列未满"的线程
return item;
} finally {
lock.unlock();
}
}
}
Condition 的优势:可以精确唤醒特定类型的等待线程,而不是 notifyAll() 唤醒所有人(减少无效竞争)。
五、synchronized vs ReentrantLock 全面对比
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 锁的获取 | 不可中断,一直等 | 可中断(lockInterruptibly())、可超时(tryLock(timeout))、立即返回(tryLock()) |
| 公平锁 | 不支持(仅非公平) | 支持(构造函数传入 true) |
| 释放锁 | 自动释放(代码块结束/JVM 保证) | 必须手动 unlock()(通常在 finally) |
| Condition | 只有一个等待队列 | 可以创建多个 Condition,精确唤醒 |
| 性能 | JDK 6 之后大幅优化,与 ReentrantLock 接近 | 功能更多,低竞争时略逊于 synchronized |
| 使用难度 | 简单,不容易出错 | 复杂,忘记 unlock 会死锁 |
| 可重入 | ✅ 支持 | ✅ 支持 |
| 底层实现 | JVM 层面(monitorenter/monitorexit 指令) | JDK 层面(AQS — AbstractQueuedSynchronizer) |
六、底层原理简要了解
面试可能会问两者的实现原理,这里简单说一下。
6.1 synchronized 的实现 — 监视器锁(Monitor)
synchronized 的底层是 ObjectMonitor,每个 Java 对象都有一个 Monitor 与之关联。
Java 对象头(Mark Word)
│
├── 无锁状态
├── 偏向锁(Biased Locking)
├── 轻量级锁(Thin Lock)
└── 重量级锁(Heavyweight Lock)← 真正的 Monitor
锁升级过程(JDK 6 优化):
无锁
↓ (第一次加锁,且只有一个线程)
偏向锁(在对象头记录线程ID,几乎零开销)
↓ (有第二个线程竞争)
轻量级锁(CAS 自旋,避免系统调用)
↓ (自旋超过一定次数,或的竞争加剧)
重量级锁(OS 级别的互斥锁,开销最大)
💡 这就是为什么 JDK 6 之后
synchronized性能大幅提升,不再比ReentrantLock差多少。
6.2 ReentrantLock 的实现 — AQS
ReentrantLock 内部使用 AQS(AbstractQueuedSynchronizer) 实现。
ReentrantLock
│
├── Sync(抽象内部类,继承 AQS)
│ │
│ ├── NonfairSync(非公平锁)
│ └── FairSync(公平锁)
│
└── state(AQS 的同步状态)
│
├── state = 0 → 锁空闲
└── state = N → 被同一个线程重入了 N 次
AQS 的核心思想:
- 用一个
volatile int state表示锁状态 - 获取锁:
CAS(state, 0, 1),失败则进入等待队列 - 释放锁:
state--,state=0 时唤醒等待队列中的下一个线程
七、生产环境怎么选?
优先用 synchronized,以下情况才考虑 ReentrantLock:
需要选择锁的实现时
│
├── 需要可中断等待? → ReentrantLock
├── 需要尝试获取锁(不阻塞)? → ReentrantLock
├── 需要公平锁? → ReentrantLock
├── 需要多个等待条件? → ReentrantLock + Condition
│
└── 都不需要?
→ **用 synchronized**(简单、不容易出错)
具体建议:
| 场景 | 推荐 |
|---|---|
| 简单的同步块,竞争不激烈 | synchronized |
| 需要超时获取锁 | ReentrantLock.tryLock(timeout) |
| 需要精确唤醒特定线程 | ReentrantLock + Condition |
| 读多写少(高性能) | ReentrantReadWriteLock 或 StampedLock(比前两者更强) |
💡 延伸:读多写少的场景,
synchronized和ReentrantLock都是排他锁(读写互斥),性能不好。应该用读写锁ReentrantReadWriteLock,后面有时间单独写一篇。
八、常见面试题整理
Q1:synchronized 和 ReentrantLock 的区别?
答:从以下角度回答:锁获取方式(可中断/tryLock)、公平锁支持、Condition、释放方式、底层实现、性能。参考上面的对比表。
Q2:synchronized 是公平锁吗?
答:不是。synchronized 只支持非公平锁。当一个线程释放锁时,任何等待的线程都有机会抢到锁,不按等待顺序。
Q3:synchronized 和 Lock 性能哪个好?
答:JDK 5 及之前 ReentrantLock 性能明显更好。JDK 6 引入偏向锁、轻量级锁后,synchronized 性能大幅提升,两者在低竞争场景下差距不大。高竞争场景下 ReentrantLock 略好,但差距有限。
实际选择时更多看功能需求,而不是性能。
Q4:ReentrantLock 忘记 unlock() 会怎样?
答:死锁。这个线程持有的锁永远不会释放,其他线程永远等待。所以 unlock() 必须放在 finally 块里。
// ✅ 正确写法
lock.lock();
try {
// ...
} finally {
lock.unlock();
}
Q5:什么是 AQS?
答:AQS(AbstractQueuedSynchronizer)是 JUC 包的核心基础框架,ReentrantLock、CountDownLatch、Semaphore 等都是基于 AQS 实现的。核心是用一个 volatile int state 表示同步状态,配合 CAS 和等待队列实现线程同步。
Q6:偏向锁、轻量级锁、重量级锁分别是什么?
| 锁状态 | 适用场景 | 开销 |
|---|---|---|
| 偏向锁 | 只有一个线程访问同步块 | 几乎零开销(只记录线程 ID) |
| 轻量级锁 | 多线程交替执行,竞争不激烈 | CAS 自旋,避免系统调用 |
| 重量级锁 | 多线程同时竞争 | OS 互斥锁,涉及用户态/内核态切换,开销最大 |
锁只能升级,不能降级(偏向 → 轻量 → 重量)。
九、总结
把 synchronized 和 ReentrantLock 的核心区别总结成一句话:
synchronized 简单够用,ReentrantLock 功能强大但需要手动管理。
选择建议:
能用 synchronized 就用 synchronized
│
└── 需要以下高级功能才换 ReentrantLock:
- 可中断获取锁
- 超时获取锁(tryLock with timeout)
- 公平锁
- 多个等待条件(Condition)
参考资料
- 《Java 并发编程实战》(Brian Goetz)
- 《深入理解 Java 虚拟机》(周志明)— 锁优化章节
- Java 官方文档 — ReentrantLock
- 美团技术博客相关文章
并发编程这块坑真的很多,我也是一边学一边踩。如果有写错的地方,欢迎评论区指正!如果觉得有帮助,点个赞呗 😊
更多推荐



所有评论(0)