04Lock 接口及实现类详解:从 ReentrantLock 到并发场景实践
在 Java 并发编程领域,同步机制是保障线程安全的核心,而我们最熟悉的莫过于 synchronized 关键字。但自 JDK 5 引入 java.util.concurrent.locks.Lock 接口及其实现类后,Java 并发编程的灵活性得到了质的提升。Lock 接口凭借“显式控制”“可中断”“公平性配置”“多条件通信”等独有的特性,完美弥补了 synchronized 的局限性,成为复杂并发场景(如高并发调度、多线程协作)的首选方案。
本文将从 Lock 接口的核心设计入手,深入拆解 ReentrantLock(可重入锁)、ReentrantReadWriteLock(读写分离锁)的底层原理与核心特性,全面对比 Lock 与 synchronized 的差异,结合实战案例讲解如何正确使用 Lock 解决并发问题、规避死锁,同时补充面试高频考点,助力开发者吃透 Lock 机制,从容应对面试与生产实战。
一、Lock 接口:同步机制的抽象定义,打破 synchronized 的局限
Lock 接口是 Java 并发包对“锁”机制的抽象封装,它将锁的“获取”与“释放”解耦为独立的方法,摆脱了 synchronized 对代码块、方法的绑定,为开发者提供了更精细的控制能力。与 synchronized 的“隐式同步”不同,Lock 采用“显式同步”,开发者需手动控制锁的获取与释放,这既是灵活性的体现,也对编码严谨性提出了更高要求。
1.1 核心方法深度解析(附使用场景)
Lock 接口的核心方法定义了锁的基本操作,每个方法都对应特定的并发场景,理解其细节是正确使用 Lock 的关键,以下结合场景逐一解析:
| 方法 | 功能描述 | 关键特性 | 适用场景 |
|---|---|---|---|
void lock() |
获取锁,若锁已被其他线程占用,则当前线程阻塞,直到获取到锁 | 不可中断,与 synchronized 的阻塞逻辑一致,无超时机制 | 简单同步场景,无需中断、无需超时,仅需基础互斥 |
void lockInterruptibly() throws InterruptedException |
获取锁,若锁被占用则阻塞,但允许线程在等待过程中响应中断(如调用 Thread.interrupt()) |
可中断,中断后抛出 InterruptedException,线程可放弃等待 |
需要取消任务的场景(如用户取消操作、任务超时),避免线程无限阻塞 |
boolean tryLock() |
尝试获取锁,无论成功与否立即返回结果,不阻塞线程 | 非阻塞,成功返回 true,失败返回 false,无等待时间 |
无需等待锁的场景,如“尝试获取锁,失败则执行备用逻辑” |
boolean tryLock(long time, TimeUnit unit) throws InterruptedException |
在指定时间内尝试获取锁,超时未获取则返回 false,等待过程中可响应中断 |
结合超时与可中断特性,兼顾灵活性与安全性 | 避免死锁的核心场景(如多锁竞争),防止线程无限等待 |
void unlock() |
释放锁,将锁归还给锁池,供其他线程竞争 | 必须手动调用,且需放在 finally 块中,否则会导致锁泄漏 |
所有 Lock 使用场景的必备操作,确保锁在任何情况下都能释放 |
Condition newCondition() |
创建一个条件变量,用于线程间的协作通信(类似 synchronized 的wait()/notify()) |
支持多条件等待,可精准唤醒指定条件的线程,避免无效唤醒 | 多线程协作场景(如生产者-消费者、任务调度) |
核心设计思想:Lock 接口的本质是“将锁的操作标准化、灵活化”——它不绑定具体的代码结构,允许开发者根据业务需求,灵活选择锁的获取方式(阻塞、非阻塞、超时、可中断),同时通过条件变量实现更精细的线程协作,这也是它与 synchronized 最核心的区别。
1.2 与 synchronized 的本质区别(面试高频)
很多面试中会问到“Lock 与 synchronized 的区别”,除了表面的“显式/隐式”,更核心的差异体现在底层实现、控制粒度和功能扩展上,以下是全面对比(结合底层原理):
- 获取与释放的显式性: synchronized 是隐式同步,线程进入 synchronized 代码块/方法时自动获取锁,退出时(正常退出或异常退出)自动释放锁,无需手动干预; Lock 是显式同步,必须手动调用
lock()(或其重载方法)获取锁,调用unlock()释放锁,且unlock()必须放在finally块中,否则会导致锁泄漏(线程异常退出时锁未释放,其他线程无法获取)。 - 灵活性: synchronized 仅支持“不可中断、无超时、非公平”的锁获取,功能单一,无法满足复杂场景需求; Lock 支持可中断、超时获取、公平/非公平锁配置、多条件通信,灵活性极高,可适配各种复杂并发场景。
- 底层实现: synchronized 是 JVM 层面的实现(底层由 C++ 代码编写,依赖操作系统的互斥量 Mutex),锁升级(偏向锁→轻量级锁→重量级锁)由 JVM 自动完成; Lock 是 Java 代码层面的实现(基于 AQS 框架——AbstractQueuedSynchronizer,队列同步器),锁的逻辑由 Java 代码控制,可自定义扩展(如自定义锁实现)。
- 性能: 低并发、无竞争场景下,两者性能接近(synchronized 经过 JDK 6 优化后,性能大幅提升); 高并发、高竞争场景下,Lock(如 ReentrantLock)性能更稳定,因为它避免了 synchronized 升级为重量级锁后的内核态切换开销,且支持更灵活的锁策略。
补充面试点:Lock 不能完全替代 synchronized——synchronized 是 JVM 层面的同步,可被 JVM 优化(如锁消除、锁粗化),且无需手动释放锁,不易出错;而 Lock 需手动控制锁的释放,编码不当易导致死锁或锁泄漏,因此需根据场景选择。
二、ReentrantLock:可重入锁的经典实现,Lock 接口的核心落地
ReentrantLock 是 Lock 接口最常用、最经典的实现类,其名称中的“Reentrant”(可重入)是它的核心特性——即同一个线程可以多次获取同一把锁,这与 synchronized 的可重入性一致,保证了线程在重入同步代码时不会死锁。
ReentrantLock 底层基于 AQS 框架实现,通过“状态变量(state)”记录线程获取锁的次数,实现可重入性;通过“等待队列”管理阻塞的线程,实现公平/非公平锁的控制,是复杂并发场景中最常用的锁实现。
2.1 基本使用方法(必掌握,避坑关键)
ReentrantLock 的使用必须遵循“获取 - 使用 - 释放”的固定模式,核心是“unlock() 必须放在finally 块中”——无论临界区代码是否抛出异常,都能确保锁被释放,避免锁泄漏。
public class ReentrantLockDemo {
// 1. 创建ReentrantLock实例(默认非公平锁,可传入true创建公平锁)
private final Lock lock = new ReentrantLock();
// 共享资源(需同步保护)
private int count = 0;
// 同步方法:增加计数器
public void increment() {
// 2. 获取锁(可根据场景选择lock()、lockInterruptibly()等方法)
lock.lock();
try {
// 3. 临界区操作(修改共享资源)
count++;
System.out.println("当前计数器:" + count + ",当前线程:" + Thread.currentThread().getName());
} finally {
// 4. 释放锁:必须放在finally块中,确保锁一定被释放
lock.unlock();
}
}
// 同步方法:获取计数器
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockDemo demo = new ReentrantLockDemo();
// 多线程测试
for (int i = 0; i < 3; i++) {
new Thread(() -> {
for (int j = 0; j < 2; j++) {
demo.increment();
}
}, "线程" + (i+1)).start();
}
}
}
关键注意事项(避坑重点):
- 切勿忘记调用
unlock():若临界区代码抛出异常,且unlock()未放在finally块中,锁会被当前线程永久持有,其他线程无法获取,导致死锁; - 可重入性的正确使用:同一线程多次调用
lock()后,必须调用相同次数的unlock()才能完全释放锁(如调用 2 次lock(),需调用 2 次unlock()); - Lock 实例建议用
final修饰:避免 Lock 实例被修改(如赋值为 null),导致锁失效,引发线程安全问题。
2.2 核心特性详解(底层+实战,面试重点)
ReentrantLock 的核心特性的是它优于 synchronized 的关键,也是面试高频考点,以下结合底层实现和实战示例,逐一拆解。
2.2.1 可重入性(底层原理+示例)
可重入性:同一个线程可以多次获取同一把锁,无需担心死锁。例如,线程 A 获取锁后,再次调用需要同一把锁的方法,可直接获取锁,无需阻塞。
底层实现原理:ReentrantLock 内部通过 AQS 的 state 变量(int 类型)记录线程获取锁的次数:
- 线程首次获取锁时,将
state从 0 改为 1,并记录当前持有锁的线程 ID; - 线程再次获取锁时,判断当前线程 ID 与持有锁的线程 ID 一致,将
state加 1; - 线程释放锁时,将
state减 1,当state变为 0 时,锁才真正释放,供其他线程竞争。
实战示例(可重入性验证):
public class ReentrantDemo {
// 非公平锁实例
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
// 线程首次获取锁
lock.lock();
try {
System.out.println("第一次获取锁,state = 1");
// 线程再次获取锁(可重入)
lock.lock();
try {
System.out.println("第二次获取锁,state = 2");
// 可继续重入,state 继续加 1
lock.lock();
try {
System.out.println("第三次获取锁,state = 3");
} finally {
lock.unlock(); // 第三次释放,state = 2
}
} finally {
lock.unlock(); // 第二次释放,state = 1
}
} finally {
lock.unlock(); // 第一次释放,state = 0,锁真正释放
}
}
}
运行结果:依次输出“第一次获取锁”“第二次获取锁”“第三次获取锁”,无阻塞,验证了可重入性。若不可重入,线程第二次获取锁时会阻塞,导致死锁。
2.2.2 公平性控制(公平锁 vs 非公平锁)
ReentrantLock 支持两种锁模式,通过构造函数指定,这是它与 synchronized(仅非公平锁)的核心区别之一:
// 1. 非公平锁(默认):线程获取锁的顺序不保证与请求顺序一致,允许“插队”
Lock nonFairLock = new ReentrantLock();
// 2. 公平锁:线程获取锁的顺序与请求顺序一致,先请求的线程先获取(FIFO)
Lock fairLock = new ReentrantLock(true);
公平性的底层权衡(面试重点):
- 公平锁: 实现原理:通过 AQS 的“等待队列”维护线程的请求顺序,新请求的线程会加入队列尾部,只有队列头部的线程才能获取锁; 优势:避免线程饥饿(某些线程长期无法获取锁),保证调度公平性; 劣势:性能较差——需要维护队列的顺序,频繁切换线程,增加开销。
- 非公平锁: 实现原理:新请求的线程会先尝试“插队”获取锁(直接 CAS 修改 state),若失败再加入等待队列; 优势:性能更好——避免了队列维护和线程切换的开销,适合高并发场景; 劣势:可能导致线程饥饿(某些线程长期插队,其他线程无法获取锁)。
适用场景:
- 公平锁:对公平性要求高的场景(如资源调度系统、任务队列调度),需保证每个线程都能公平获取资源;
- 非公平锁:追求高性能的一般场景(如计数器、缓存更新),默认选择即可,非公平锁的性能通常比公平锁高 10%~20%。
2.2.3 可中断的锁获取(解决无限阻塞问题)
synchronized 的锁获取是“不可中断”的——一旦线程阻塞在 synchronized 代码块前,除非持有锁的线程释放锁,否则该线程会一直阻塞,无法响应中断。而 ReentrantLock 的 lockInterruptibly() 方法,允许线程在等待锁的过程中响应中断,避免无限期阻塞。
实战示例(可中断锁的使用):
public class InterruptibleLockDemo {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
// 线程1:尝试获取可中断锁
Thread t1 = new Thread(() -> {
try {
// 可中断地获取锁:等待过程中若被中断,会抛出InterruptedException
lock.lockInterruptibly();
try {
System.out.println("线程1获取到锁,开始执行耗时操作");
Thread.sleep(1000); // 模拟耗时操作
} finally {
lock.unlock(); // 确保锁释放
System.out.println("线程1释放锁");
}
} catch (InterruptedException e) {
// 捕获中断异常,线程放弃获取锁,执行后续逻辑
System.out.println("线程1被中断,放弃获取锁");
e.printStackTrace();
}
}, "线程1");
// 主线程先获取锁,让线程1阻塞等待
lock.lock();
t1.start();
Thread.sleep(200); // 让线程1进入阻塞等待状态
t1.interrupt(); // 中断线程1的等待
lock.unlock(); // 主线程释放锁(即使线程1被中断,也要释放锁,避免锁泄漏)
}
}
运行结果解析:
线程1调用 lockInterruptibly() 后,因主线程持有锁而阻塞;主线程调用 t1.interrupt() 后,线程1被中断,抛出 InterruptedException,执行 catch 块逻辑,放弃获取锁,避免了无限阻塞。
应用场景:任务取消、超时控制(如用户取消正在执行的任务,线程可中断等待锁的过程,快速释放资源)。
2.2.4 超时获取锁(避免死锁的核心手段)
ReentrantLock 的 tryLock(long time, TimeUnit unit) 方法,允许线程在指定时间内尝试获取锁:若在超时时间内获取到锁,返回 true;若超时未获取到锁,返回 false,线程可放弃等待,执行备用逻辑,这是避免死锁的核心手段。
实战示例(超时获取锁):
public class TimeoutLockDemo {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
// 线程1:尝试在1秒内获取锁
Thread t1 = new Thread(() -> {
try {
// 尝试在1秒内获取锁,超时则返回false
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("线程1获取到锁,开始执行");
Thread.sleep(2000); // 模拟持有锁2秒
} finally {
lock.unlock();
System.out.println("线程1释放锁");
}
} else {
// 超时未获取到锁,执行备用逻辑
System.out.println("线程1超时(1秒),未获取到锁");
}
} catch (InterruptedException e) {
System.out.println("线程1获取锁时被中断");
e.printStackTrace();
}
}, "线程1");
// 主线程先获取锁,持有1.5秒
lock.lock();
t1.start();
Thread.sleep(1500); // 主线程持有锁1.5秒,超过线程1的超时时间(1秒)
lock.unlock();
}
}
运行结果:线程1尝试获取锁时,主线程持有锁1.5秒,超过线程1的超时时间(1秒),因此线程1输出“超时未获取到锁”,不会无限阻塞。
关键注意:超时时间的设置需结合业务场景——时间过短可能导致频繁获取锁失败,时间过长则无法有效避免死锁,通常设置为 1~5 秒(根据业务耗时调整)。
2.3 条件变量(Condition)的使用(多线程协作神器)
ReentrantLock 通过 newCondition() 方法创建 Condition 对象,实现线程间的协作通信,相比 synchronized 的 wait()/notify(),Condition 支持“多条件等待”,能精准唤醒指定条件的线程,避免无效唤醒,提升并发效率。
核心对比:synchronized 只有一个“条件队列”,调用 notifyAll() 会唤醒所有等待的线程,即使某些线程不满足唤醒条件,也会被唤醒,造成无效调度;而 Condition 可以为每个条件创建一个独立的队列,调用 signal() 仅唤醒该条件队列中的线程,精准高效。
实战示例:用 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 ConditionDemo {
// 锁实例
private final Lock lock = new ReentrantLock();
// 条件1:队列非空(消费者等待该条件,生产者唤醒)
private final Condition notEmpty = lock.newCondition();
// 条件2:队列非满(生产者等待该条件,消费者唤醒)
private final Condition notFull = lock.newCondition();
// 共享队列(生产者生产数据,消费者消费数据)
private final Queue<Integer> queue = new LinkedList<>();
// 队列容量
private static final int CAPACITY = 5;
// 生产者:生产数据,放入队列
public void put(int value) throws InterruptedException {
lock.lock();
try {
// 队列满时,生产者等待(等待“非满”条件)
while (queue.size() == CAPACITY) {
System.out.println("队列已满,生产者等待...");
notFull.await(); // 释放锁,进入notFull条件队列等待
}
// 生产数据,放入队列
queue.add(value);
System.out.println("生产:" + value + ",队列大小:" + queue.size());
// 唤醒等待“非空”条件的消费者(队列有数据了)
notEmpty.signal();
} finally {
lock.unlock();
}
}
// 消费者:从队列中消费数据
public int take() throws InterruptedException {
lock.lock();
try {
// 队列空时,消费者等待(等待“非空”条件)
while (queue.isEmpty()) {
System.out.println("队列为空,消费者等待...");
notEmpty.await(); // 释放锁,进入notEmpty条件队列等待
}
// 消费数据,从队列中取出
int value = queue.poll();
System.out.println("消费:" + value + ",队列大小:" + queue.size());
// 唤醒等待“非满”条件的生产者(队列有空闲位置了)
notFull.signal();
return value;
} finally {
lock.unlock();
}
}
// 测试生产者-消费者模式
public static void main(String[] args) {
ConditionDemo demo = new ConditionDemo();
// 3个生产者线程
for (int i = 0; i < 3; i++) {
int producerId = i;
new Thread(() -> {
try {
for (int j = 0; j < 3; j++) {
demo.put(producerId * 10 + j);
Thread.sleep(500); // 模拟生产耗时
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "生产者" + (i+1)).start();
}
// 2个消费者线程
for (int i = 0; i < 2; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 4; j++) {
demo.take();
Thread.sleep(800); // 模拟消费耗时
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "消费者" + (i+1)).start();
}
}
}
运行结果解析:
当队列满时,生产者调用 notFull.await() 进入等待状态,释放锁;当消费者消费数据后,调用 notFull.signal() 唤醒生产者,继续生产;同理,队列空时,消费者等待,生产者生产后唤醒消费者,实现了高效的线程协作。
核心优势:Condition 将“队列满”和“队列空”两个条件分离,避免了 synchronized 中 notifyAll() 唤醒所有线程的无效开销,提升了并发效率,是多线程协作场景的首选方案。
三、ReentrantReadWriteLock:读写分离的锁机制,优化读多写少场景
在实际开发中,很多场景存在“读多写少”的特点(如缓存读取、配置查询、日志查看)——读操作可以并发执行(多个线程同时读,不会导致数据不一致),而写操作需要独占访问(同一时间只能有一个线程写,否则会导致数据错乱)。
ReentrantLock 和 synchronized 都是“独占锁”(同一时间只能有一个线程持有锁),在“读多写少”场景下,会导致读操作相互阻塞,严重影响并发性能。而 ReentrantReadWriteLock(读写锁)通过“分离读锁和写锁”,实现“读共享、写独占”,大幅提升读多写少场景的吞吐量。
3.1 核心特性(底层+关键规则)
ReentrantReadWriteLock 实现了 Lock 接口的子接口 ReadWriteLock,包含两个核心锁:ReadLock(读锁,共享锁)和 WriteLock(写锁,独占锁),其核心特性如下:
- 读写分离:读锁可被多个线程同时持有(共享),写锁只能被一个线程持有(独占);
- 可重入性:读锁和写锁都支持可重入——同一线程可多次获取读锁,也可多次获取写锁(需对应次数释放);
- 锁降级支持:写锁可降级为读锁(流程:先获取写锁 → 再获取读锁 → 最后释放写锁),但读锁不能升级为写锁(避免死锁);
- 公平性控制:支持公平锁和非公平锁(构造函数传入 true 为公平锁),默认非公平锁。
关键:锁的兼容性规则(面试高频,必须掌握)——判断新请求的锁能否获取,取决于当前持有锁的类型,具体规则如下:
| 当前持有锁 | 新请求的锁 | 能否获取 | 说明 |
|---|---|---|---|
| 无锁 | 读锁 | 能 | 多个线程可同时获取读锁,实现读共享 |
| 无锁 | 写锁 | 能 | 写锁独占,只有一个线程能获取 |
| 读锁(多个线程持有) | 读锁 | 能 | 读锁共享,新线程可直接获取 |
| 读锁(多个线程持有) | 写锁 | 不能 | 写锁需独占,需等待所有读锁释放 |
| 写锁(单个线程持有) | 读锁 | 能 | 同一线程可获取读锁,实现锁降级 |
| 写锁(单个线程持有) | 写锁 | 能 | 同一线程可重入写锁,其他线程不能获取 |
3.2 基本使用方法(读锁 vs 写锁)
ReentrantReadWriteLock 的使用需分别获取读锁和写锁,遵循“读操作使用读锁,写操作使用写锁”的原则,释放锁同样需放在 finally 块中。
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
// 1. 创建ReentrantReadWriteLock实例(默认非公平锁)
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 2. 获取读锁(共享锁)
private final Lock readLock = rwLock.readLock();
// 3. 获取写锁(独占锁)
private final Lock writeLock = rwLock.writeLock();
// 共享资源:缓存(读多写少场景)
private Map<String, Object> cache = new HashMap<>();
// 读操作:查询缓存,使用读锁
public Object get(String key) {
readLock.lock(); // 获取读锁
try {
System.out.println("线程" + Thread.currentThread().getName() + "读取key:" + key);
// 模拟读操作耗时
try { Thread.sleep(100); } catch (InterruptedException e) {}
return cache.get(key);
} finally {
readLock.unlock(); // 释放读锁
}
}
// 写操作:更新缓存,使用写锁
public void put(String key, Object value) {
writeLock.lock(); // 获取写锁
try {
System.out.println("线程" + Thread.currentThread().getName() + "写入key:" + key + ",value:" + value);
// 模拟写操作耗时
try { Thread.sleep(200); } catch (InterruptedException e) {}
cache.put(key, value);
} finally {
writeLock.unlock(); // 释放写锁
}
}
// 测试读多写少场景
public static void main(String[] args) {
ReadWriteLockDemo demo = new ReadWriteLockDemo();
// 5个读线程(并发读)
for (int i = 0; i < 5; i++) {
int readId = i;
new Thread(() -> {
demo.get("key" + readId);
}, "读线程" + (readId+1)).start();
}
// 2个写线程(独占写)
for (int i = 0; i < 2; i++) {
int writeId = i;
new Thread(() -> {
demo.put("key" + writeId, "value" + writeId);
}, "写线程" + (writeId+1)).start();
}
}
}
运行结果特点:
- 5个读线程可以同时执行读操作,无需相互阻塞,提升了读并发效率;
- 写线程执行写操作时,所有读线程和其他写线程都会阻塞,确保写操作的原子性;
- 写操作完成后,读线程才能继续执行,保证数据一致性。
性能优势:在高并发读场景下,ReentrantReadWriteLock 的吞吐量远高于 ReentrantLock 和 synchronized——例如,100个读线程并发执行时,ReentrantReadWriteLock 可支持100个线程同时读,而 ReentrantLock 只能让线程串行执行,性能差距显著。
3.3 锁降级示例(实战场景+底层意义)
锁降级是 ReentrantReadWriteLock 的核心特性之一,指“写锁持有者先获取读锁,再释放写锁”,最终持有读锁的过程。锁降级的核心意义是:确保写操作完成后,读操作能立即看到最新数据,且不会被其他写操作中断。
注意:锁降级不能反向(读锁不能升级为写锁)——如果多个线程持有读锁,其中一个线程尝试获取写锁,会导致该线程阻塞,直到所有读锁释放,若此时其他线程继续获取读锁,会导致死锁。
实战示例(锁降级的使用):
public class LockDowngradeDemo {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private String data = "初始数据";
// 锁降级流程:写锁 → 读锁 → 释放写锁 → 释放读锁
public void downgradeLock() {
// 1. 获取写锁(独占)
writeLock.lock();
try {
System.out.println("获取写锁,开始更新数据");
// 更新数据(写操作)
data = "更新后的数据";
// 2. 获取读锁(同一线程,可在持有写锁时获取读锁)
readLock.lock();
System.out.println("获取读锁,完成锁降级");
} finally {
// 3. 释放写锁(此时仍持有读锁,其他线程可获取读锁,但不能获取写锁)
writeLock.unlock();
System.out.println("释放写锁,保留读锁");
}
// 4. 持有读锁,执行读操作
try {
System.out.println("持有读锁,读取数据:" + data);
// 模拟读操作耗时
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 5. 释放读锁,锁完全释放
readLock.unlock();
System.out.println("释放读锁,锁完全释放");
}
}
public static void main(String[] args) {
LockDowngradeDemo demo = new LockDowngradeDemo();
new Thread(demo::downgradeLock, "锁降级线程").start();
}
}
运行结果解析:
线程先获取写锁,更新数据后,获取读锁,再释放写锁——此时线程仍持有读锁,可安全读取最新数据,且其他线程可获取读锁(共享),但无法获取写锁(需等待读锁释放),确保了数据一致性和读并发效率。
应用场景:写操作后需要立即读取数据,且希望后续读操作能并发执行(如缓存更新后,立即读取缓存,同时允许其他线程读取)。
四、Lock 与 synchronized 的全面对比及选择指南(实战必备)
在实际开发中,选择 Lock 还是 synchronized,核心取决于业务场景——没有“最优”的锁,只有“最合适”的锁。以下是两者的全面对比,以及具体场景的选择建议。
4.1 功能对比(表格清晰呈现,面试必背)
| 特性 | Lock(以 ReentrantLock 为例) | synchronized |
|---|---|---|
| 可重入性 | 支持 | 支持 |
| 公平性 | 可设置公平/非公平锁 | 仅非公平锁 |
| 锁获取方式 | 显式(lock()/unlock()) | 隐式(代码块/方法) |
| 可中断性 | 支持(lockInterruptibly()) | 不支持 |
| 超时获取 | 支持(tryLock(time)) | 不支持 |
| 条件变量 | 支持多条件(Condition) | 仅单条件(wait()/notify()) |
| 性能 | 高竞争场景下更优,读多写少场景可配合 ReentrantReadWriteLock 优化 | 低竞争场景下接近 Lock,高竞争场景下略差 |
| 灵活性 | 高(可自定义扩展,支持多种锁策略) | 低(固定实现,无扩展能力) |
| 锁泄漏风险 | 有(忘记 unlock() 会导致锁泄漏) | 无(JVM 自动释放锁) |
4.2 适用场景选择指南(实战落地)
结合业务场景,给出明确的选择建议,避免盲目使用 Lock 或 synchronized:
优先使用 synchronized 的场景
- 简单同步场景:如单例模式、简单计数器、少量共享资源修改,语法简洁,不易出错;
- 无复杂需求场景:无需中断、无需超时、无需多条件通信,仅需基础互斥;
- 单线程或低并发场景:性能差异可忽略,synchronized 更简洁,且 JVM 可自动优化(锁消除、锁粗化)。
优先使用 ReentrantLock 的场景
- 需要中断等待锁的线程:如用户取消操作、任务超时,需通过 lockInterruptibly() 中断线程;
- 需要超时获取锁:避免死锁(如多锁竞争场景),通过 tryLock(time) 设置超时时间;
- 需要多条件变量通信:如生产者-消费者、任务调度,通过 Condition 实现精准唤醒;
- 需要公平锁:对线程调度公平性要求高的场景(如资源调度系统)。
优先使用 ReentrantReadWriteLock 的场景
- 读多写少场景:如缓存读取、配置查询、日志查看,读操作远多于写操作;
- 需要提升读并发性能:希望多个线程同时执行读操作,避免读操作相互阻塞。
五、实战案例:用 ReentrantLock 解决死锁问题(生产级方案)
死锁是并发编程中的常见问题,其产生的核心条件是“多个线程持有不同的锁,且相互等待对方释放锁”。synchronized 由于不支持超时和中断,一旦发生死锁,只能通过重启应用解决;而 ReentrantLock 的 tryLock() 方法(超时获取),可有效打破死锁的“循环等待”条件,避免死锁。
场景描述
两个线程分别需要获取两把锁(lockA 和 lockB),但获取顺序相反:线程1先获取 lockA,再获取 lockB;线程2先获取 lockB,再获取 lockA。使用 synchronized 会导致死锁,用 ReentrantLock 的 tryLock() 可完美解决。
解决方案(生产级代码,含注释)
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockSolution {
// 定义两把锁
private final Lock lockA = new ReentrantLock();
private final Lock lockB = new ReentrantLock();
// 线程1的操作:先获取lockA,再获取lockB
public void operation1() throws InterruptedException {
// 1. 超时尝试获取lockA(1秒超时),避免无限等待
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("线程" + Thread.currentThread().getName() + "获取到lockA");
Thread.sleep(100); // 模拟业务操作,持有lockA一段时间
// 2. 超时尝试获取lockB(1秒超时)
if (lockB.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("线程" + Thread.currentThread().getName() + "获取到lockB,执行核心操作");
// 核心业务逻辑
} finally {
// 3. 释放lockB
lockB.unlock();
System.out.println("线程" + Thread.currentThread().getName() + "释放lockB");
}
} else {
// 4. 获取lockB超时,释放已获取的lockA,打破循环等待
System.out.println("线程" + Thread.currentThread().getName() + "获取lockB超时,释放lockA");
}
} finally {
// 5. 释放lockA(无论是否获取到lockB,都要释放lockA)
lockA.unlock();
System.out.println("线程" + Thread.currentThread().getName() + "释放lockA");
}
} else {
// 6. 获取lockA超时,放弃操作
System.out.println("线程" + Thread.currentThread().getName() + "获取lockA超时,放弃操作");
}
}
// 线程2的操作:先获取lockB,再获取lockA
public void operation2() throws InterruptedException {
if (lockB.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("线程" + Thread.currentThread().getName() + "获取到lockB");
Thread.sleep(100);
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("线程" + Thread.currentThread().getName() + "获取到lockA,执行核心操作");
} finally {
lockA.unlock();
System.out.println("线程" + Thread.currentThread().getName() + "释放lockA");
}
} else {
System.out.println("线程" + Thread.currentThread().getName() + "获取lockA超时,释放lockB");
}
} finally {
lockB.unlock();
System.out.println("线程" + Thread.currentThread().getName() + "释放lockB");
}
} else {
System.out.println("线程" + Thread.currentThread().getName
更多推荐




所有评论(0)