Java 中已经有 synchronized,为什么还要提供 Lock?
面向的是“高级并发控制”这类问题:
synchronized能用,但不够精细。Lock是一把“可编程的锁”。
1. 回顾:synchronized 能做什么?
sychronized 是 Java 最早提供的同步机制,有三种用法:
// 1. 修饰代码块
synchronized (lockObj) {
// 临界区
}
// 2. 修饰实例方法(锁的是 this)
public synchronized void foo() { ... }
// 3. 修饰静态方法(锁的是 Class 对象)
public static synchronized void bar() { ... }
它的特点:
- 互斥:同一把锁同一时刻只允许一个线程进入临界区。
- 可重入:同一个线程可以多次获得同一把锁,不会死锁。
- 可见性 + 有序性:释放锁前会把工作内存中的数据刷新到主内存,获得锁后会从主内存重新读。
- JVM 层实现,语法简单:出错点少。
但是:
- 一旦阻塞就只能傻等,没有超时、尝试获取的能力。
- 不支持公平锁(严格按等待顺序获取)。
- 只能有一个隐式条件队列(
wait/notify),不能方便地维护多个等待条件。 - 锁的获取与释放是“语法绑死”的:进入
synchronized自动加锁,离开块自动释放,无法进行特别灵活的控制。
这些限制在简单并发场景里没问题,但在复杂、高并发框架 / 中间件中就不够用了。
2. Lock 接口是什么?
Lock 是 java.util.concurrent.locks 包里的一个接口,最常用的实现类是:
ReentrantLock:可重入互斥锁ReentrantReadWriteLock:读写锁StampedLock:支持乐观读锁
它把“锁”从 JVM 语法层面抽象成了一个普通对象,可以像操作普通对象一样来控制锁:
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}
这个写法和 synchronized 相比,更啰嗦,但换来了更大的控制力。
3. Lock 相对于 synchronized 的核心优势
3.1 支持可中断锁获取(lockInterruptibly)
问题场景:
线程 A 在拿锁时堵住了,这时你想取消这个线程(比如用户取消了操作),synchronized 做不到,只能一直等锁。
Lock 提供:
lock.lockInterruptibly();
如果线程在等待锁的过程中被 interrupt(),会立刻抛出 InterruptedException,线程可以优雅退出。
适用场景:
- 做 RPC、远程调用、数据库操作等时,如果锁等待过长且业务允许取消,就可以用可中断锁避免线程“僵死”。
3.2 支持尝试获取 / 超时获取(tryLock)
问题场景:
一段代码想拿锁,但如果一时拿不到,不想死等,可以:
- 马上放弃做别的事情;
- 或者只等待一段时间,超时就算了。
Lock 提供:
if (lock.tryLock()) {
try {
// 获取到锁,执行逻辑
} finally {
lock.unlock();
}
} else {
// 没拿到锁,做降级、返回友好提示等
}
支持超时版本:
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
// 半秒内拿到锁
} finally {
lock.unlock();
}
} else {
// 半秒没拿到锁,放弃
}
适用场景:
- 高并发下避免“锁饥饿”:拿不到就快速返回,不阻塞线程池。
- 做限流、降级时用“试探性”获取锁。
sychronized 完全不支持这些细粒度策略。
3.3 支持公平锁(new ReentrantLock(true))
sychronized 的锁是非公平锁:后来的线程可能插队先拿到锁,吞吐量更高,但可能造成部分线程长期拿不到锁(饥饿)。
Lock 允许你选择:
Lock fairLock = new ReentrantLock(true); // 公平锁
Lock unfairLock = new ReentrantLock(false); // 默认非公平
公平锁会按等待时间顺序唤醒线程,类似排队叫号,牺牲吞吐,换取更可预期的调度。
适用场景:
- 业务对“公平性”“请求顺序”要求很高的地方(如队列、排队系统)。
3.4 支持多个条件队列(Condition)
synchronized + wait/notify 有几个问题:
- 每个锁只有一个等待队列,所有等待都混在一起。
notify叫醒的是“某一个”线程,不可控。notifyAll又容易叫醒一堆不相关的线程,造成“惊群效应”。
Lock 提供 Condition:
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
你可以:
// 线程 A 等待 notEmpty 条件
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
// 取元素
} finally {
lock.unlock();
}
// 线程 B 生产元素后只唤醒等待 notEmpty 的线程
lock.lock();
try {
queue.add(xxx);
notEmpty.signal(); // 或 signalAll()
} finally {
lock.unlock();
}
这样就可以把“等待队列”按业务拆分:
- 等待“队列不空”的一组线程;
- 等待“队列不满”的另一组线程;
不同条件互不影响,逻辑更清晰。
适用场景:
- 自己实现阻塞队列、连接池、资源池等复杂并发容器时。
3.5 支持读写锁、乐观读等高级玩法
Lock 体系中还有:
ReentrantReadWriteLock:读写分离- 多线程读可以并发进行,读写、写写互斥。
- 读多写少场景下大幅提高吞吐量。
StampedLock:提供乐观读锁- 读的时候可以“大胆假设数据不变”,先乐观读,读完检查一下是否被写入修改,没被改则直接用。
synchronized 无法表达这些高级策略。
3.6 锁释放更灵活(但也更危险)
synchronized 的一个优点:离开代码块就自动释放锁,不会忘记。
Lock 则是手动释放:
lock.lock();
try {
// ...
} finally {
lock.unlock(); // 必须写
}
这在某些场景很有用:
- 可以把
unlock()放在不同的方法里,实现更灵活的生命周期控制; - 可以在同一方法中多次
lock/unlock控制临界区范围;
但也有风险:
- 忘记写
unlock(),就会导致死锁或长期占用锁。
4. synchronized 与 Lock 的对比总结
| 维度 | synchronized | Lock(如 ReentrantLock) |
|---|---|---|
| 实现层 | JVM 关键字 | Java 层接口 + 实现类 |
| 可重入 | 支持 | 支持(ReentrantLock) |
| 公平锁 | 不支持 | 支持(构造函数可选公平) |
| 可中断获取锁 | 不支持 | 支持 lockInterruptibly() |
| 尝试/超时获取锁 | 不支持 | 支持 tryLock() / tryLock(timeout) |
| 条件队列 | 只有一个(wait/notify) | 支持多个 Condition |
| 读写锁 / 乐观读 | 不支持 | 支持(ReentrantReadWriteLock, StampedLock) |
| 性能 | JDK8 之后整体很优秀,大量优化 | 实现灵活,部分场景性能更好 |
| 锁释放 | 自动(离开作用域) | 手动 unlock(),易出错 |
| 使用难度 | 简单,适合一般业务逻辑 | 较复杂,适合框架 / 中间件 / 高级并发控制 |
简单记忆:
- 能用
synchronized搞定的,就别上Lock; - 但一旦遇到:
- 需要“可中断/可超时”的获取锁;
- 需要读写分离;
- 需要多个条件队列;
- 需要公平锁;
这时候就该上 Lock 系列。
5. 示例对比:阻塞队列实现
5.1 用 synchronized + wait/notify
public class MyBlockingQueueWithSync<E> {
private final Queue<E> queue = new LinkedList<>();
private final int capacity;
public MyBlockingQueueWithSync(int capacity) {
this.capacity = capacity;
}
public synchronized void put(E e) throws InterruptedException {
while (queue.size() == capacity) {
wait(); // 队满,等待
}
queue.add(e);
notifyAll(); // 通知消费者可能有元素了
}
public synchronized E take() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // 队空,等待
}
E e = queue.remove();
notifyAll(); // 通知生产者可能有空间了
return e;
}
}
问题:一个 wait() 队列,notifyAll() 会惊扰所有等待的线程,效率较低。
5.2 用 ReentrantLock + Condition
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyBlockingQueueWithLock<E> {
private final Queue<E> queue = new LinkedList<>();
private final int capacity;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public MyBlockingQueueWithLock(int capacity) {
this.capacity = capacity;
}
public void put(E e) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // 只在“队满队列”里等待
}
queue.add(e);
notEmpty.signal(); // 唤醒一个“队空队列”的等待者
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 只在“队空队列”里等待
}
E e = queue.remove();
notFull.signal(); // 唤醒一个“队满队列”的等待者
return e;
} finally {
lock.unlock();
}
}
}
这里:
- 生产者只在
notFull队列里等待/被唤醒; - 消费者只在
notEmpty队列里等待/被唤醒; - 没有无关线程被唤醒,效率更高。
实际 JDK 的 ArrayBlockingQueue、LinkedBlockingQueue 就是基于 ReentrantLock + Condition 实现的。
6. 实战选择建议
-
默认优先用
synchronized- 业务代码里大多数“给某个方法/代码块加锁”的需求,
synchronized足够稳、简单、难出 bug。
- 业务代码里大多数“给某个方法/代码块加锁”的需求,
-
在这些场景考虑
Lock:- 需要:
- 尝试获取锁 / 超时获取锁;
- 可中断的锁获取;
- 公平锁;
- 多个条件队列;
- 读写锁、乐观读;
- 或者你在实现:
- 线程池、队列、连接池、缓存容器等高并发基础组件;
- 对性能、调度公平性要求比较高的底层模块。
- 需要:
-
使用
Lock时的注意点:lock()后,一定要在finally里unlock():lock.lock(); try { // ... } finally { lock.unlock(); }- 搭配
tryLock使用时,只有在true分支里才unlock()。 - 注意死锁风险:多个锁要按固定顺序获取。
7. 总结一句话
Java 提供
Lock,不是为了替代synchronized,而是为了补齐它在“可中断、可超时、公平性、多条件队列、读写分离”等高级并发场景里的短板。简单用synchronized,复杂用Lock。
1114

被折叠的 条评论
为什么被折叠?



