【Java并发编程】Lock体系全解析:ReentrantLock、ReentrantReadWriteLock与StampedLock
前言
在Java并发编程中,除了大家熟知的synchronized关键字,java.util.concurrent.locks包还提供了一套强大的显式锁体系。如果说synchronized是自动挡汽车,简单易用但功能有限,那么ReentrantLock就是手动挡——你拥有完全控制权,但一旦忘了换挡(忘记解锁),车子就会熄火(死锁)。
JDK 5引入了java.util.concurrent.locks包,以Lock接口为核心,ReentrantLock为主要实现,底层依托AQS(抽象队列同步器)和CAS两大核心技术,实现了更灵活、高效的同步机制。
本文将从「使用场景→API细节→底层原理→源码解析→扩展锁→实战避坑」全方位拆解Java显式锁体系,帮你彻底掌握多种锁的使用方法,应对面试高频考点和实际开发难题。
一、Lock接口:JDK层面的锁规范
1.1 Lock接口的核心方法
| 方法 | 核心作用 | 使用场景 | 注意事项 |
|---|---|---|---|
void lock() | 获取锁,若锁被占用则阻塞 | 基础同步场景,无需中断、超时 | 不可中断,若线程被阻塞只能等待锁释放 |
void lockInterruptibly() | 可响应中断地获取锁 | 避免线程永久阻塞,如超时任务 | 必须捕获InterruptedException |
boolean tryLock() | 非阻塞尝试获取锁,立即返回 | 无锁竞争时快速获取,避免阻塞 | 获取失败不阻塞,需手动判断是否重试 |
boolean tryLock(long time, TimeUnit unit) | 超时获取锁 | 防止线程无限等待,如网络请求 | 时间单位需明确,支持中断 |
void unlock() | 释放锁,必须手动调用 | 所有获取锁的场景 | 必须在finally中调用,防止锁泄漏 |
Condition newCondition() | 创建条件变量,实现线程精准唤醒 | 多条件等待/唤醒(如生产者-消费者模型) | Condition的await()/signal()需在锁范围内调用 |
1.2 Lock相比synchronized的优势
| 特性 | synchronized | Lock(ReentrantLock) |
|---|---|---|
| 锁实现层面 | JVM内置,依赖对象头 | JDK层面,依赖AQS |
| 释放方式 | 自动释放 | 手动释放,必须在finally中unlock() |
| 可中断性 | 不可中断 | 支持lockInterruptibly() |
| 超时获取 | 不支持 | 支持tryLock(timeout) |
| 公平锁 | 不支持 | 可通过构造参数指定公平/非公平 |
| 条件等待 | 单隐式等待队列(wait/notify) | 支持多个Condition对象,更灵活 |
二、ReentrantLock:可重入锁的典范
2.1 什么是“可重入”?
“Reentrant”(可重入)的意思是:如果一个线程已经持有这把锁,它可以再次获取同一把锁而不被阻塞。这对递归调用或类内部方法互相调用非常关键。
2.2 基本使用
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private final Lock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // 加锁
try {
count++; // 访问共享资源
} finally {
lock.unlock(); // 解锁——必须在finally中执行
}
}
}
⚠️ 铁律:永远用
try-finally块,这是代码评审中的铁律。如果在持有锁的情况下发生异常,必须确保锁被正确释放,否则可能导致死锁。
2.3 构造器与公平性
// 默认非公平锁(性能更高)
ReentrantLock lock = new ReentrantLock();
// 指定公平锁(true=公平,false=非公平)
ReentrantLock fairLock = new ReentrantLock(true);
ReentrantLock有两个构造器,默认实现的是非公平锁,可以在构造函数参数中指定公平锁。
所谓锁是否公平,简单理解就是一系列线程获取到锁的顺序是否遵循「先来后到」。即,如果先申请锁的线程先获取到锁,就是公平锁;否则就是非公平锁。
- 公平锁:严格按FIFO顺序分配锁,避免线程饥饿,但吞吐量较低
- 非公平锁:允许插队,性能更高,但可能导致某些线程长时间等待
2.4 高级特性使用示例
tryLock:非阻塞式获取
public void tryDoSomething() {
if (lock.tryLock()) { // 立即返回,不会阻塞
try {
// 执行操作
} finally {
lock.unlock();
}
} else {
// 锁被占用,执行备选方案
System.out.println("锁被占用,稍后重试");
}
}
tryLock + 超时机制
public boolean tryDoWithTimeout() {
try {
if (lock.tryLock(2, TimeUnit.SECONDS)) { // 最多等待2秒
try {
// 执行操作
return true;
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
return false;
}
lockInterruptibly:可响应中断的获取锁
public void doInterruptibly() throws InterruptedException {
lock.lockInterruptibly(); // 等待过程中可被中断
try {
// 执行操作
} finally {
lock.unlock();
}
}
2.5 可重入性演示
public class ReentrantDemo {
private final Lock lock = new ReentrantLock();
public void outerMethod() {
lock.lock();
try {
System.out.println("外层方法持有锁");
innerMethod(); // 再次获取同一把锁,不会死锁
} finally {
lock.unlock();
}
}
public void innerMethod() {
lock.lock(); // 同一线程第二次获取锁
try {
System.out.println("内层方法也持有锁");
} finally {
lock.unlock();
}
}
}
三、Condition:多条件队列的精准唤醒
3.1 为什么需要Condition?
传统的synchronized只能配合Object.wait()/notify()/notifyAll()使用,所有等待线程共享同一个隐式等待队列,notifyAll()会唤醒所有等待线程,造成“惊群效应”。
ReentrantLock通过newCondition()可以为同一把锁创建多个独立的Condition实例,每个Condition对应一个独立的等待队列。这让你能按不同业务逻辑或状态对线程进行分组等待和唤醒。
3.2 典型场景:生产者-消费者模式
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.LinkedList;
import java.util.Queue;
public class BoundedBuffer<T> {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 队列不满条件
private final Condition notEmpty = lock.newCondition(); // 队列不空条件
private final Queue<T> buffer = new LinkedList<>();
private final int capacity;
public BoundedBuffer(int capacity) {
this.capacity = capacity;
}
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (buffer.size() == capacity) {
notFull.await(); // 队列满时,生产者等待
}
buffer.offer(item);
notEmpty.signal(); // 唤醒等待的消费者
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (buffer.isEmpty()) {
notEmpty.await(); // 队列空时,消费者等待
}
T item = buffer.poll();
notFull.signal(); // 唤醒等待的生产者
return item;
} finally {
lock.unlock();
}
}
}
3.3 使用Condition的注意事项
- 必须在持有锁时调用
await()/signal(),否则抛出IllegalMonitorStateException await()会自动释放锁,被唤醒后重新获取锁后才返回await()必须在循环中调用,防止虚假唤醒signal()唤醒一个等待线程,signalAll()唤醒全部
四、底层原理:AQS(AbstractQueuedSynchronizer)
4.1 AQS是什么?
AQS是Java提供的一个用于构建锁和同步器的框架,ReentrantLock、Semaphore、CountDownLatch等并发工具都是基于AQS实现的。
AQS的核心由三部分组成:
state属性:表示同步状态,使用volatile修饰并通过CAS更新- FIFO等待队列(CLH队列改良版):管理等待获取锁的线程
- ConditionObject:支持多条件等待队列
4.2 state:同步状态
private volatile int state;
AQS使用一个volatile的int类型成员变量state来表示同步状态,通过CAS完成对state值的修改来获得锁。
不同子类对state的语义定义不同:
| 同步器 | state的含义 |
|---|---|
ReentrantLock | 0=未加锁,1=加锁一次,>1=同一线程重入次数 |
ReentrantReadWriteLock | 高16位代表读锁状态,低16位代表写锁状态 |
Semaphore | 可用信号的个数 |
CountDownLatch | 计数器的值 |
4.3 CLH等待队列
当线程抢不到锁时,会被封装成一个Node节点加入FIFO队列。队列是双向链表,入队遵循FIFO原则,解锁时唤醒队头节点继续竞争。
// Node节点的关键字段
volatile int waitStatus; // 节点状态
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 当前线程
Node nextWaiter; // 条件队列中的后继节点
waitStatus的几个重要取值:
| 常量 | 值 | 含义 |
|---|---|---|
CANCELLED | 1 | 线程已取消(等待超时或被中断) |
SIGNAL | -1 | 后继线程需要被唤醒 |
CONDITION | -2 | 节点在Condition等待队列中 |
PROPAGATE | -3 | 共享模式需传播 |
| 0 | 0 | 初始状态 |
4.4 公平锁与非公平锁的实现差异
ReentrantLock通过内部抽象类Sync继承AQS,再由NonfairSync和FairSync两个子类分别实现公平和非公平策略。
非公平锁逻辑(默认):
线程请求锁时,不管有没有线程等待,先CAS抢一次锁;抢不到才入队排队等待。性能更高,因为减少了排队。
公平锁逻辑:
线程请求锁时,检查队列头后面是否有节点;如果有,乖乖排队入队等待。唤醒时按照顺序依次获得锁。
4.5 可重入性的实现原理
ReentrantLock通过AQS的state字段记录持有锁的次数来实现可重入:
- 第一次加锁:
state从0设为1 - 重入加锁:
state递增(例如从1变成2) - 解锁:
state递减,仅当state归零时才真正释放锁
五、锁的演进:ReentrantReadWriteLock与StampedLock
5.1 ReentrantReadWriteLock:读锁共享,写锁独占
在“读多写少”的场景下(如商品目录API,每分钟万名用户查价格,管理员每天更新一次),如果用ReentrantLock,每个读操作都会互相阻塞,完全是资源浪费。
ReentrantReadWriteLock把锁拆成两种:
- 读锁(共享锁):多个线程可以同时持有
- 写锁(独占锁):只有一个人能写,且写的时候不能有人读
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteCache<K, V> {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
private final java.util.Map<K, V> cache = new java.util.HashMap<>();
public V get(K key) {
readLock.lock(); // 读锁,多个线程可同时进入
try {
return cache.get(key);
} finally {
readLock.unlock();
}
}
public void put(K key, V value) {
writeLock.lock(); // 写锁,独占
try {
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
}
⚠️ 注意:
ReentrantReadWriteLock存在一个潜在问题——写锁饥饿。如果读线程持续不断地获取读锁,写线程可能会一直等待,无法获得写锁。
5.2 StampedLock:乐观读与性能之王
StampedLock是JDK 8引入的读写锁优化版本,它提供了三种锁模式:
| 模式 | 特点 | 适用场景 |
|---|---|---|
| 写锁(Write Lock) | 独占,任何时刻只允许一个线程持有 | 数据修改操作 |
| 悲观读锁(Read Lock) | 共享,会阻塞写锁 | 需要严格一致性的读操作 |
| 乐观读(Optimistic Read) | 无锁读取,不阻塞写锁 | 读多写少、对数据一致性要求不苛刻的场景 |
乐观读的核心思想
乐观读允许线程在不获取任何锁的情况下读取共享资源。在乐观读期间,如果有写线程获取了写锁,乐观读就会失效。因此,乐观读需要在读取完成后进行验证,以确保数据的一致性。
import java.util.concurrent.locks.StampedLock;
public class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
// 写锁:独占写入
public void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
// 乐观读(推荐用于读多写少场景)
public double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); // 获取乐观读锁(不实际阻塞)
double currentX = x, currentY = y;
if (!sl.validate(stamp)) { // 验证:乐观读期间是否有写锁发生?
// 写锁发生了,需要升级为悲观读锁
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// 锁升级:从读锁升级为写锁
public void moveIfAtOrigin(double newX, double newY) {
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) {
long ws = sl.tryConvertToWriteLock(stamp);
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
} else {
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}
}
StampedLock的乐观读机制可以显著提高读多写少场景下的吞吐量,尤其适合缓存、配置数据等场景。
六、总结与选型建议
6.1 各锁类型对比
| 特性 | synchronized | ReentrantLock | ReentrantReadWriteLock | StampedLock |
|---|---|---|---|---|
| 锁类型 | 互斥锁 | 互斥锁 | 读锁共享/写锁独占 | 写锁/读锁/乐观读 |
| 公平锁 | ❌ | ✅ 可选 | ✅ 可选 | ❌ |
| 可中断 | ❌ | ✅ | ✅ | ✅ |
| 超时获取 | ❌ | ✅ | ✅ | ✅ |
| Condition | ❌ (用wait/notify) | ✅ 多Condition | ✅ 与ReentrantLock相同 | ❌ |
| 乐观读 | ❌ | ❌ | ❌ | ✅ |
| JDK版本 | 1.0 | 5 | 5 | 8 |
6.2 选型决策树
是否需要复杂功能(公平/中断/超时/多条件)?
├─ 不需要 → 使用 synchronized(简单,安全,自动释放)
└─ 需要 → 使用 ReentrantLock
是否是读多写少场景?
├─ 是 → 是否需要极致性能(乐观读)?
│ ├─ 是,且JDK 8+ → 使用 StampedLock(乐观读+读锁+写锁)
│ └─ 否 → 使用 ReentrantReadWriteLock
└─ 否 → 使用 ReentrantLock
6.3 最佳实践提醒
unlock()必须在finally中调用,这是不可妥协的铁律await()调用必须在循环中进行条件判断,防止虚假唤醒- 不要将
lock对象声明为public,避免外部恶意调用 ReentrantLock默认是非公平锁,没有特殊需求不要擅自改为公平锁,因为公平锁会降低吞吐量StampedLock不可重入,且不支持Condition,使用时需注意StampedLock的乐观读使用tryOptimisticRead(),不要与readLock()混淆
更多推荐




所有评论(0)