Java 提供了两套锁体系:synchronized 关键字(JVM 内置)以及 java.util.concurrent.locks 包中的显式锁接口。本文从「为什么需要锁」出发,再依次展开 Lock 接口、ReentrantLock、ReadWriteLock、StampedLock。
一、为什么使用锁
在多线程项目中,当多个线程访问公共资源时,会出现以下三类问题:
- 原子性问题:某些操作本应作为一个整体不可分割地执行(如读-改-写),但被多个线程交错拆分。
- 有序性问题:编译器和 CPU 可能对指令进行重排序。
- 可见性问题:一个线程的修改,另一个线程不一定能看到。
锁的核心作用
- 互斥:在同一时刻只允许一个线程进行操作。
- 同步:保证线程之间的执行顺序和内存可见性。
Java 中锁大致分为两类
synchronized:JVM 内置关键字,自动加锁 / 释放锁。Lock接口及其实现:java.util.concurrent(JUC)包提供的显式锁,更灵活。
二、Lock 接口
java.util.concurrent.locks.Lock 是 JUC 中锁的顶层接口。
核心方法
void lock(); // 获取锁,阻塞直到成功
void lockInterruptibly() throws InterruptedException; // 可中断获取锁
boolean tryLock(); // 尝试非阻塞获取锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 超时获取
void unlock(); // 释放锁
Condition newCondition(); // 创建条件变量
相比 synchronized 的优势
| 优势 | 说明 |
|---|---|
| 非阻塞尝试 | tryLock() 可立即返回结果,不阻塞 |
| 可中断 | lockInterruptibly() 在等待时可响应中断 |
| 超时 | tryLock(timeout, unit) 限时等待,避免永久阻塞 |
| 公平锁 | 支持公平 / 非公平策略可选 |
| 多条件变量 | 同一把锁可创建多个 Condition,实现精准的等待 / 唤醒 |
| 显式控制 | 加锁 / 解锁由代码控制,灵活处理异常路径 |
使用模板
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}
注意:unlock() 必须放在 finally 中,否则异常会导致死锁。
三、ReentrantLock
- 是什么:可重入互斥锁,基于 AQS(AbstractQueuedSynchronizer)实现。
- 可重入:一个线程可以多次加锁,每获取一次 state +1,释放一次 state -1,直到 0 才真正释放。
公平锁 vs 非公平锁
new ReentrantLock(true); // 公平锁
new ReentrantLock(false); // 非公平锁(默认)
- 公平锁:按等待队列顺序获取,避免饥饿,但吞吐量低。
- 非公平锁:允许插队,吞吐量高,但可能饥饿。
大多数场景用非公平锁。
Condition
ReentrantLock 可以创建多个 Condition 来实现更精细的唤醒。
await():当前线程释放锁并进入等待,直到被唤醒或中断。signal():唤醒一个等待中的线程。signalAll():唤醒所有等待线程。
使用方法(生产者-消费者)
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// 生产者
lock.lock();
try {
while (queue.isFull()) notFull.await();
queue.add(x);
notEmpty.signal();
} finally {
lock.unlock();
}
// 消费者
lock.lock();
try {
while (queue.isEmpty()) notEmpty.await();
queue.poll();
notFull.signal();
} finally {
lock.unlock();
}
这是实现阻塞队列的经典方式。
四、ReadWriteLock
1. 是什么
ReadWriteLock 是一个接口,核心实现是 ReentrantReadWriteLock。它维护两把锁:
- 读锁(共享锁):多个线程可同时读。
- 写锁(排他锁):写时不允许其他读或写。
规则:
- 读-读:不互斥。
- 读-写:互斥。
- 写-写:互斥。
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
// 读
readLock.lock();
try {
// 读共享数据
} finally {
readLock.unlock();
}
// 写
writeLock.lock();
try {
// 修改共享数据
} finally {
writeLock.unlock();
}
2. 特点
- 读多写少场景性能好。
- 支持锁降级:写锁 → 读锁(先获取读锁再释放写锁)。
- 不支持锁升级:读锁 → 写锁(会死锁)。
- 可重入。
- 可选择公平 / 非公平。
3. 适用场景
- 缓存:读多写少。
- 配置中心:偶尔更新,频繁读取。
- 数据结构:读操作远多于写操作。
4. 缺点
- 读多写少才划算;写多时性能不如互斥锁。
- 可能造成写饥饿(读锁一直占用)。
- 实现较复杂,容易用错。
五、StampedLock
1. 是什么
StampedLock 是 Java 8 引入的锁,位于 java.util.concurrent.locks。它有三种模式:
- 写锁(writeLock):排他。
- 悲观读锁(readLock):共享,类似 ReadWriteLock。
- 乐观读(tryOptimisticRead):不加锁,读取后校验版本号。
核心思想:乐观读 + 版本戳(stamp)校验,在读多写少且冲突少的场景下性能极高。
2. 使用方式
StampedLock lock = new StampedLock();
// 乐观读
long stamp = lock.tryOptimisticRead();
int x = this.x;
int y = this.y;
if (!lock.validate(stamp)) {
// 版本变了,升级为悲观读
stamp = lock.readLock();
try {
x = this.x;
y = this.y;
} finally {
lock.unlockRead(stamp);
}
}
// 写
long ws = lock.writeLock();
try {
this.x = newX;
this.y = newY;
} finally {
lock.unlockWrite(ws);
}
3. 特点
- 不是可重入锁:重复获取会死锁。
- 不支持
Condition。 - 乐观读不阻塞写,吞吐量高。
- stamp 必须正确使用,否则容易出错。
- 适合读多写少、读操作短小的场景。
4. 适用场景
- 读远多于写。
- 读操作很快,冲突少。
- 对吞吐量要求高。
- 能接受复杂 API 和不可重入限制。
5. 不适用场景
- 需要可重入。
- 需要
Condition。 - 写操作频繁。
- 读操作耗时长。