Lock 与显式锁详解
场景:大翔最近被一个需求搞得很头疼——飞翔科技的连接池管理系统需要实现"尝试获取资源,超时就放弃"的功能。synchronized 只能死等,根本做不到。
大翔:"synchronized 要么获取锁,要么阻塞到天荒地老。有没有能超时放弃、能被打断的锁?"
白歌(放下咖啡杯):"JDK 5 就引入了
java.util.concurrent.locks.Lock接口,把 synchronized 做不到的——tryLock超时、lockInterruptibly、公平锁——全补上了。"小崔(眼睛一亮):"那岂不是可以替代 synchronized?"
Frank:"Not exactly. Lock gives you more control, but you must manually unlock — even in the finally block. Forget unlock? You have a deadlock. synchronized automatically releases the lock."
朱璐:"而且 Lock 的实现——ReentrantLock——底层是基于 AQS(AbstractQueuedSynchronizer)框架的。理解了 AQS,就理解了整个 JUC 并发包的设计哲学。"
白歌:"不过 AQS 很深,我们今天先聚焦于 Lock 接口和 ReentrantLock 的实战用法。Condition 的生产者-消费者模式也别漏。"
大翔:"好,那就开讲。"
一、定义速查表
| 属性 | 说明 |
|---|---|
| Lock 接口 | java.util.concurrent.locks.Lock,提供比 synchronized 更灵活的锁操作。核心方法:lock(), tryLock(), lockInterruptibly(), unlock(), newCondition() |
| ReentrantLock | Lock 接口的标准实现,支持可重入、公平/非公平模式。默认非公平锁(吞吐量优先),基于 AQS 框架实现 |
| 公平锁(Fair Lock) | 按请求顺序分配锁,先到先得。new ReentrantLock(true),吞吐量较低但避免饥饿 |
| 非公平锁(Unfair Lock) | 允许"插队"获取锁,吞吐量更高。new ReentrantLock(false) 或默认构造,JDK 8 默认行为 |
| tryLock() | 尝试获取锁,立即返回 true/false;支持超时版本 tryLock(long timeout, TimeUnit unit),是 Lock 相比 synchronized 的核心优势 |
| lockInterruptibly() | 可被中断的锁获取,被中断时抛出 InterruptedException。synchronized 不可中断 |
| Condition | 条件变量,通过 lock.newCondition() 创建。提供 await(), signal(), signalAll() 方法,对应 Object 的 wait(), notify(), notifyAll() |
| AQS(AbstractQueuedSynchronizer) | JUC 并发框架的核心,基于 FIFO 双向队列 + volatile int state 实现的同步器框架。ReentrantLock、Semaphore、CountDownLatch 等均基于 AQS |
| 可重入(Reentrant) | 同一线程可多次获取同一把锁。ReentrantLock 内部维护 state 计数器和 exclusiveOwnerThread 记录持有线程 |
| 锁绑定多条件 | 一个 Lock 可以创建多个 Condition,实现更精细的线程等待/唤醒控制。synchronized 只有一个隐式条件 |
二、Mermaid 图解
2.1 ReentrantLock lock() 获取锁流程
2.2 Lock 接口类层次结构
三、synchronized vs Lock 完整对比
| 对比维度 | synchronized | Lock(ReentrantLock) |
|---|---|---|
| 实现层面 | JVM 内置,Monitor 机制 | JDK API,基于 AQS |
| 锁释放 | 自动(代码块结束/异常) | 必须手动 unlock()(finally 中) |
| 可中断获取 | ❌ 不支持 | ✅ lockInterruptibly() |
| 超时获取 | ❌ 不支持 | ✅ tryLock(timeout, unit) |
| 非阻塞尝试 | ❌ 不支持 | ✅ tryLock() |
| 公平锁 | ❌ 仅非公平 | ✅ 可选公平/非公平 |
| 条件变量 | 1 个隐式条件(wait/notify) | 多个 Condition |
| 查询锁状态 | ❌ 不支持 | ✅ isLocked(), getHoldCount() 等 |
| 性能(JDK 8) | 优化后接近 | 略优(尤其高并发) |
| 死锁风险 | 较低(自动释放) | 较高(忘记 unlock) |
| 使用复杂度 | 低 | 高 |
四、完整代码示例
示例 1:飞翔科技资源池管理 —— tryLock 超时获取
场景:飞翔科技的数据库连接池只有 3 个连接,多个业务线程竞争获取。使用 tryLock(timeout) 实现"超时获取不到就放弃"。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* 飞翔科技 - 资源池管理 tryLock 超时演示
*
* 场景:数据库连接池只有 3 个连接(每个连接一把锁)
* 多个业务线程尝试获取连接,2 秒超时放弃
* 大翔(10001)、白歌(10002)、小崔(10003)、Frank(10004)
*/
public class ResourcePoolDemo {
// 资源池:3 个连接,每个连接对应一把锁
static class ConnectionPool {
private final ReentrantLock[] locks;
private final String[] connectionNames;
public ConnectionPool(int size) {
locks = new ReentrantLock[size];
connectionNames = new String[size];
for (int i = 0; i < size; i++) {
locks[i] = new ReentrantLock();
connectionNames[i] = "DB-Conn-" + (i + 1);
}
}
/**
* 尝试获取连接,超时放弃
* @param workerName 业务线程名称
* @param timeout 超时时间(秒)
* @return 获取到的连接名称,null 表示获取失败
*/
public String acquireConnection(String workerName, long timeout) {
for (int i = 0; i < locks.length; i++) {
try {
// ★ tryLock 超时 — 尝试获取锁,超时则放弃
if (locks[i].tryLock(timeout, TimeUnit.SECONDS)) {
System.out.printf("[%s] ✓ 获取连接 %s\n", workerName, connectionNames[i]);
return connectionNames[i];
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
System.out.printf("[%s] ✗ 所有连接都被占用,超时放弃\n", workerName);
return null;
}
/**
* 释放连接
*/
public void releaseConnection(String connName) {
for (int i = 0; i < connectionNames.length; i++) {
if (connectionNames[i].equals(connName) && locks[i].isHeldByCurrentThread()) {
locks[i].unlock();
System.out.printf("[释放] %s 已归还\n", connName);
return;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
ConnectionPool pool = new ConnectionPool(3);
System.out.println("========== 飞翔科技连接池管理 (tryLock 超时) ==========\n");
System.out.println("连接池大小: 3 | 超时时间: 2秒 | 业务线程: 4\n");
// 线程1:大翔(10001) — 执行业务查询
Thread t1 = new Thread(() -> {
String conn = pool.acquireConnection("大翔-10001", 2);
if (conn != null) {
try { Thread.sleep(3000); } catch (InterruptedException e) { }
pool.releaseConnection(conn);
}
}, "大翔");
// 线程2:白歌(10002) — 执行报表生成
Thread t2 = new Thread(() -> {
String conn = pool.acquireConnection("白歌-10002", 2);
if (conn != null) {
try { Thread.sleep(3000); } catch (InterruptedException e) { }
pool.releaseConnection(conn);
}
}, "白歌");
// 线程3:小崔(10003) — 执行数据导入
Thread t3 = new Thread(() -> {
String conn = pool.acquireConnection("小崔-10003", 2);
if (conn != null) {
try { Thread.sleep(3000); } catch (InterruptedException e) { }
pool.releaseConnection(conn);
}
}, "小崔");
// 线程4:Frank(10004) — 执行数据导出(第4个线程,晚启动)
Thread t4 = new Thread(() -> {
try { Thread.sleep(500); } catch (InterruptedException e) { }
String conn = pool.acquireConnection("Frank-10004", 2);
if (conn != null) {
try { Thread.sleep(3000); } catch (InterruptedException e) { }
pool.releaseConnection(conn);
}
}, "Frank");
t1.start(); t2.start(); t3.start(); t4.start();
t1.join(); t2.join(); t3.join(); t4.join();
System.out.println("\n========== 所有业务处理完毕 ==========");
}
}
控制台输出:
========== 飞翔科技连接池管理 (tryLock 超时) ==========
连接池大小: 3 | 超时时间: 2秒 | 业务线程: 4
[大翔-10001] ✓ 获取连接 DB-Conn-1
[白歌-10002] ✓ 获取连接 DB-Conn-2
[小崔-10003] ✓ 获取连接 DB-Conn-3
[Frank-10004] ✗ 所有连接都被占用,超时放弃 ← 第4个线程超时放弃!
[释放] DB-Conn-1 已归还
[释放] DB-Conn-2 已归还
[释放] DB-Conn-3 已归还
========== 所有业务处理完毕 ==========
示例 2:飞翔科技生产者-消费者 —— Condition 精确唤醒
场景:飞翔科技的薪资计算系统,大翔(生产者)计算员工薪资(8888.88 元基数),白歌(消费者)审核发放。使用 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;
/**
* 飞翔科技 - 生产者消费者 Condition 精确唤醒
*
* 场景:大翔(10001) 计算薪资放入队列,白歌(10002) 从队列取出发放
* 队列容量 5,使用 Lock + Condition 实现
* notFull: 队列未满条件,notEmpty: 队列非空条件
*/
public class ProducerConsumerConditionDemo {
static class SalaryQueue {
private final Queue<Float> queue = new LinkedList<>();
private final int capacity;
private final Lock lock = new ReentrantLock();
// ★ 两个 Condition — 精确控制生产者和消费者的唤醒
private final Condition notFull = lock.newCondition(); // 队列未满
private final Condition notEmpty = lock.newCondition(); // 队列非空
public SalaryQueue(int capacity) {
this.capacity = capacity;
}
/**
* 生产者:大翔计算薪资并放入队列
*/
public void produce(float salary, int employeeId) throws InterruptedException {
lock.lock();
try {
// ★ while 循环防止虚假唤醒
while (queue.size() == capacity) {
System.out.printf("[生产者] 队列已满,等待消费者消费...\n");
notFull.await(); // 等待 notFull 条件
}
queue.offer(salary);
System.out.printf("[生产者-大翔] 计算工号%d 薪资: %.2f 元, 队列: %d/%d\n",
employeeId, salary, queue.size(), capacity);
// ★ 唤醒消费者 — 精确唤醒等待 notEmpty 的线程
notEmpty.signal();
} finally {
lock.unlock(); // ★ 必须在 finally 中释放锁
}
}
/**
* 消费者:白歌从队列取出薪资并发放
*/
public float consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
System.out.printf("[消费者] 队列为空,等待生产者生产...\n");
notEmpty.await(); // 等待 notEmpty 条件
}
float salary = queue.poll();
System.out.printf("[消费者-白歌] 发放薪资: %.2f 元, 队列: %d/%d\n",
salary, queue.size(), capacity);
// ★ 唤醒生产者 — 精确唤醒等待 notFull 的线程
notFull.signal();
return salary;
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
SalaryQueue salaryQueue = new SalaryQueue(5);
System.out.println("========== 飞翔科技薪资计算系统 (Condition) ==========\n");
// 生产者线程:大翔(10001)
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
int employeeId = 10001 + i;
float salary = 8888.88f + i * 100; // 不同员工的薪资
salaryQueue.produce(salary, employeeId);
Thread.sleep(300); // 模拟计算耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Producer-大翔");
// 消费者线程:白歌(10002)
Thread consumer = new Thread(() -> {
try {
float totalPaid = 0;
for (int i = 0; i < 10; i++) {
float salary = salaryQueue.consume();
totalPaid += salary;
Thread.sleep(500); // 模拟发放耗时
}
System.out.printf("\n[消费者-白歌] 总计发放薪资: %.2f 元\n", totalPaid);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Consumer-白歌");
producer.start();
consumer.start();
producer.join();
consumer.join();
System.out.println("\n========== 本月薪资发放完毕 ==========");
}
}
控制台输出:
========== 飞翔科技薪资计算系统 (Condition) ==========
[生产者-大翔] 计算工号10001 薪资: 8888.88 元, 队列: 1/5
[生产者-大翔] 计算工号10002 薪资: 8988.88 元, 队列: 2/5
[消费者-白歌] 发放薪资: 8888.88 元, 队列: 1/5
[生产者-大翔] 计算工号10003 薪资: 9088.88 元, 队列: 2/5
[消费者-白歌] 发放薪资: 8988.88 元, 队列: 1/5
[生产者-大翔] 计算工号10004 薪资: 9188.88 元, 队列: 2/5
[生产者-大翔] 计算工号10005 薪资: 9288.88 元, 队列: 3/5
[消费者-白歌] 发放薪资: 9088.88 元, 队列: 2/5
...(生产消费交替进行)
[生产者-大翔] 计算工号10010 薪资: 9788.88 元, 队列: 1/5
[消费者-白歌] 发放薪资: 9788.88 元, 队列: 0/5
[消费者-白歌] 总计发放薪资: 93388.80 元
========== 本月薪资发放完毕 ==========
五、ReentrantLock 高级特性
5.1 公平锁 vs 非公平锁
/**
* 飞翔科技 - 公平锁 vs 非公平锁对比
*/
public class FairVsUnfairDemo {
public static void main(String[] args) {
// 公平锁:先到先得
ReentrantLock fairLock = new ReentrantLock(true);
// 非公平锁:允许插队(默认)
ReentrantLock unfairLock = new ReentrantLock(false);
System.out.println("========== 飞翔科技锁模式对比 ==========\n");
// 公平锁测试
System.out.println("--- 公平锁 ---");
testLock("公平锁", fairLock);
// 非公平锁测试
System.out.println("\n--- 非公平锁 ---");
testLock("非公平锁", unfairLock);
}
private static void testLock(String label, ReentrantLock lock) {
for (int i = 0; i < 5; i++) {
final int workerId = 10001 + i;
new Thread(() -> {
System.out.printf("[工号%d] 请求 %s...\n", workerId, label);
lock.lock();
try {
System.out.printf("[工号%d] ✓ 获得 %s\n", workerId, label);
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}).start();
}
try { Thread.sleep(2000); } catch (InterruptedException e) { }
}
}
5.2 lockInterruptibly 可中断获取
/**
* 飞翔科技 - lockInterruptibly 可中断锁获取演示
*/
public class LockInterruptDemo {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
// 大翔(10001) 先获取锁并长时间持有
Thread holding = new Thread(() -> {
lock.lock();
try {
System.out.println("[大翔-10001] 获取锁,将持有 5 秒...");
Thread.sleep(5000);
System.out.println("[大翔-10001] 释放锁");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}, "大翔-10001");
// 小崔(10003) 尝试获取锁,但可以被中断
Thread waiting = new Thread(() -> {
try {
System.out.println("[小崔-10003] 尝试获取锁(可中断)...");
lock.lockInterruptibly(); // ★ 可被中断的锁获取
try {
System.out.println("[小崔-10003] 获取锁成功");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("[小崔-10003] 等待被中断,放弃获取锁");
}
}, "小崔-10003");
holding.start();
Thread.sleep(500); // 确保大翔先获取锁
waiting.start();
Thread.sleep(1000); // 等 1 秒后中断小崔
System.out.println("[管理员] 中断小崔的等待...");
waiting.interrupt();
holding.join();
waiting.join();
System.out.println("\n========== 演示结束 ==========");
}
}
六、易错场景
❌ 错误 1:忘记在 finally 中释放锁
// ✗ 错误:如果临界区抛异常,unlock() 不会执行 → 锁永远不会释放!
ReentrantLock lock = new ReentrantLock();
lock.lock();
doSomething(); // 如果这里抛异常...
lock.unlock(); // ...这行不会执行!
// ✓ 正确:unlock 必须在 finally 中
lock.lock();
try {
doSomething();
} finally {
lock.unlock(); // 无论如何都会释放锁
}
❌ 错误 2:Condition 的 await 用 if 而不是 while
// ✗ 错误:if 只检查一次,虚假唤醒后不会重新检查条件
lock.lock();
try {
if (queue.isEmpty()) {
condition.await(); // 被虚假唤醒后直接往下执行,可能队列仍为空!
}
queue.poll(); // 可能抛出 NoSuchElementException
} finally {
lock.unlock();
}
// ✓ 正确:使用 while 循环,每次唤醒都重新检查条件
lock.lock();
try {
while (queue.isEmpty()) { // ★ while 循环防虚假唤醒
condition.await();
}
queue.poll();
} finally {
lock.unlock();
}
❌ 错误 3:signal() 和 signalAll() 用错
// ✗ 错误:多生产者-多消费者场景用 signal() 可能唤醒同类型线程导致死锁
// 例如:消费者唤醒消费者,两者都等待 notEmpty → 永远无法唤醒
notEmpty.signal(); // 可能唤醒另一个消费者
// ✓ 正确:不确定时统一使用 signalAll()
notEmpty.signalAll(); // 唤醒所有等待线程,让它们竞争
❌ 错误 4:tryLock 后不检查返回值
// ✗ 错误:tryLock 返回 false 时仍然操作共享资源
ReentrantLock lock = new ReentrantLock();
lock.tryLock(); // 忽略了返回值!
sharedResource++; // ⚠ 可能没有获取到锁!
lock.unlock(); // ⚠ 可能抛出 IllegalMonitorStateException
// ✓ 正确:必须在 tryLock 成功后才操作
if (lock.tryLock()) {
try {
sharedResource++;
} finally {
lock.unlock();
}
} else {
// 处理获取失败的情况
}
七、面试考点
Q1:Lock 和 synchronized 的区别?什么时候用 Lock?
A:
| 维度 | synchronized | Lock |
|---|---|---|
| 获取锁 | 阻塞式,不可中断 | 可超时、可中断、可非阻塞尝试 |
| 释放 | 自动 | 手动(finally) |
| 公平性 | 非公平 | 可选公平/非公平 |
| 条件 | 1 个隐式条件 | 多个 Condition |
使用 Lock 的场景:
- 需要
tryLock超时获取(如连接池、资源池) - 需要可中断的锁获取(
lockInterruptibly) - 需要公平锁(避免线程饥饿)
- 需要多个条件变量(如复杂生产者-消费者)
- 需要查询锁状态(
isLocked(),getHoldCount()等)
使用 synchronized 的场景:
- 简单的临界区保护
- 不想手动管理锁释放
- 代码简洁性优先
Q2:ReentrantLock 的公平锁和非公平锁实现区别?
A:两者都基于 AQS 实现,区别在 tryAcquire 方法:
- 非公平锁(默认):调用
lock()时先 CAS 尝试抢锁(compareAndSetState(0, 1)),失败再进入 AQS 队列排队。已入队的线程可能被新来的线程"插队" - 公平锁:调用
lock()时直接进入 AQS 队列,只有队列为空或当前线程是第一个等待者时才 CAS 抢锁。严格按 FIFO 顺序
非公平锁吞吐量更高(减少线程切换),公平锁避免饥饿。JDK 8 默认非公平。
Q3:Condition 的 await/signal 与 Object 的 wait/notify 有什么区别?
A:
| 对比维度 | Condition | Object wait/notify |
|---|---|---|
| 所属对象 | Lock 创建 | Object 内置 |
| 需要持有锁 | Lock.lock() | synchronized |
| 多条件支持 | ✅ 一个 Lock 多个 Condition | ❌ 一个对象一个等待集 |
| 精确唤醒 | ✅ signal() 唤醒特定 Condition 的等待线程 | ❌ notify() 随机唤醒一个 |
| 中断响应 | await() 响应中断 | wait() 响应中断 |
Q4:什么是 AQS?它的核心设计思想是什么?
A:AQS(AbstractQueuedSynchronizer)是 JUC 并发框架的核心基类,基于 FIFO 双向队列 + volatile int state 实现:
- state:volatile int,表示同步状态(锁的持有计数、信号量许可数等)
- CLH 队列:双向链表,存储等待线程的 Node 节点
- 模板方法模式:
tryAcquire(),tryRelease()等由子类实现,acquire(),release()等由 AQS 提供
ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等均基于 AQS。理解 AQS 是理解整个 JUC 包的关键。
大翔(满意地合上笔记本):"Lock 的强大在于灵活——tryLock 超时、Condition 多条件、lockInterruptibly 可中断——这些都是 synchronized 做不到的。"
白歌:"但灵活是把双刃剑。记住:unlock 必须放 finally,await 必须用 while 循环。"
Frank:"And underneath it all is AQS — the framework that powers the entire java.util.concurrent package. Master AQS, and you master JUC."
朱璐:"下一章我们回到基础——wait/notify。虽然 Lock 更强大,但理解 Object 的内置等待/通知机制是面试必考题。"