在 Java 并发编程中,除了synchronized关键字,java.util.concurrent.locks.Lock接口及其实现类是另一种重要的同步机制。自 JDK 5 引入以来,Lock接口凭借灵活的 API 设计、可中断的锁获取、公平性控制等特性,成为复杂并发场景的首选方案。本文将从Lock接口的核心方法入手,深入解析ReentrantLock、ReentrantReadWriteLock等实现类的工作原理,对比其与synchronized的差异,并通过实战案例展示如何在实际开发中正确使用。
一、Lock 接口:同步机制的抽象定义
Lock接口是 Java 并发包对锁机制的抽象,它将锁的获取与释放等操作封装为显式方法,相比synchronized的隐式操作,提供了更高的灵活性。
1.1 核心方法解析
Lock接口的核心方法定义了锁的基本操作,理解这些方法是使用Lock的基础:
|
方法 |
功能描述 |
关键特性 |
|
void lock() |
获取锁,若锁被占用则阻塞 |
不可中断,与synchronized类似 |
|
void lockInterruptibly() throws InterruptedException |
获取锁,可响应中断 |
允许线程在等待锁时被中断(如Thread.interrupt()) |
|
boolean tryLock() |
尝试获取锁,立即返回结果 |
非阻塞,成功返回true,失败返回false |
|
boolean tryLock(long time, TimeUnit unit) throws InterruptedException |
超时尝试获取锁 |
结合了超时等待与可中断特性 |
|
void unlock() |
释放锁 |
必须在finally块中调用,避免锁泄漏 |
|
Condition newCondition() |
创建条件变量 |
用于线程间的协作通信 |
核心设计思想:Lock接口将锁的 “获取” 与 “释放” 解耦为独立方法,开发者需手动控制这两个操作,这既带来了灵活性,也要求更严谨的编码(如必须在finally中释放锁)。
1.2 与 synchronized 的本质区别
Lock接口与synchronized的核心差异体现在控制粒度和功能扩展上:
- 获取与释放的显式性:synchronized的锁获取与释放是隐式的(进入代码块自动获取,退出自动释放),而Lock需要手动调用lock()和unlock();
- 灵活性:Lock支持中断、超时、公平性设置等,而synchronized仅支持最基本的互斥;
- 底层实现:synchronized是 JVM 层面的实现(依赖 C++ 代码),Lock是 Java 代码层面的实现(基于 AQS 框架)。
二、ReentrantLock:可重入锁的经典实现
ReentrantLock是Lock接口最常用的实现类,其名称中的 “Reentrant” 表示可重入性—— 即线程可以多次获取同一把锁,这与synchronized的特性一致。
2.1 基本使用方法
ReentrantLock的使用遵循 “获取 - 使用 - 释放” 的模式,释放操作必须放在finally块中,确保锁在任何情况下都能被释放:
public class ReentrantLockDemo {
private final Lock lock = new ReentrantLock(); // 创建ReentrantLock实例
private int count = 0;
public void increment() {
lock.lock(); // 获取锁
try {
count++; // 临界区操作
} finally {
lock.unlock(); // 释放锁,必须在finally中执行
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
注意事项:
- 若忘记调用unlock(),会导致锁永久持有,其他线程无法获取,造成死锁;
- 同一线程多次调用lock()后,必须调用相同次数的unlock()才能完全释放锁(可重入特性)。
2.2 核心特性详解
2.2.1 可重入性
ReentrantLock允许线程重复获取锁,获取次数与释放次数必须一致:
public class ReentrantDemo {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
System.out.println("第一次获取锁");
lock.lock(); // 再次获取锁(可重入)
try {
System.out.println("第二次获取锁");
} finally {
lock.unlock(); // 第二次释放
}
} finally {
lock.unlock(); // 第一次释放
}
}
}
实现原理:ReentrantLock内部通过计数器记录线程获取锁的次数,每次lock()计数器加 1,unlock()计数器减 1,当计数器为 0 时,锁才真正释放。
2.2.2 公平性控制
ReentrantLock支持公平锁与非公平锁两种模式,通过构造函数指定:
// 非公平锁(默认):线程获取锁的顺序不保证与请求顺序一致,可能存在插队
Lock nonFairLock = new ReentrantLock();
// 公平锁:线程获取锁的顺序与请求顺序一致,先请求的线程先获取
Lock fairLock = new ReentrantLock(true);
公平性的权衡:
- 公平锁:避免线程饥饿(某些线程长期无法获取锁),但性能较差(需要维护等待队列的顺序);
- 非公平锁:性能更好(允许插队,减少线程切换开销),但可能导致某些线程长时间等待。
适用场景:
- 对公平性要求高的场景(如资源调度系统)使用公平锁;
- 追求高性能的一般场景使用非公平锁(默认)。
2.2.3 可中断的锁获取
lockInterruptibly()方法允许线程在等待锁的过程中响应中断,避免无限期阻塞:
public class InterruptibleLockDemo {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
lock.lockInterruptibly(); // 可中断地获取锁
try {
Thread.sleep(1000); // 模拟耗时操作
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("线程1被中断,放弃获取锁");
}
});
lock.lock(); // 主线程先获取锁
t1.start();
Thread.sleep(200);
t1.interrupt(); // 中断线程1的等待
lock.unlock(); // 释放主线程的锁
}
}
运行结果:线程 1 在等待锁时被中断,执行catch块逻辑,避免永久阻塞。
2.2.4 超时获取锁
tryLock(long time, TimeUnit unit)方法允许线程在指定时间内尝试获取锁,超时未获取则返回false:
public class TimeoutLockDemo {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
// 尝试在1秒内获取锁
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("线程1获取到锁");
Thread.sleep(2000); // 持有锁2秒
} finally {
lock.unlock();
}
} else {
System.out.println("线程1超时未获取到锁");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
lock.lock();
t1.start();
Thread.sleep(1500); // 主线程持有锁1.5秒
lock.unlock();
}
}
运行结果:线程 1 等待 1 秒后仍未获取锁,输出 “超时未获取到锁”(主线程 1.5 秒后才释放锁)。
2.3 条件变量(Condition)的使用
ReentrantLock通过newCondition()方法创建Condition对象,实现线程间的灵活通信,相比synchronized的wait()/notify(),Condition支持多条件等待。
示例:生产者 - 消费者模式
public class ConditionDemo {
private final Lock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition(); // 非空条件
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) {
notFull.await(); // 等待非满条件
}
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()) {
notEmpty.await(); // 等待非空条件
}
int value = queue.poll();
System.out.println("消费:" + value + ",队列大小:" + queue.size());
notFull.signal(); // 唤醒等待非满条件的线程
return value;
} finally {
lock.unlock();
}
}
}
优势:Condition将不同的等待条件分离(如 “队列满” 和 “队列空”),避免了synchronized中notifyAll()唤醒所有线程导致的效率问题。
三、ReentrantReadWriteLock:读写分离的锁机制
在多线程场景中,读操作往往可以并发执行(无线程安全问题),而写操作需要独占访问。ReentrantReadWriteLock通过分离读锁与写锁,实现 “读多写少” 场景下的性能优化。
3.1 核心特性
- 读写分离:包含ReadLock(读锁)和WriteLock(写锁),读锁可被多个线程同时持有,写锁是独占的;
- 可重入性:读锁和写锁都支持重入;
- 降级支持:写锁可降级为读锁(先获取写锁,再获取读锁,最后释放写锁),但读锁不能升级为写锁。
锁的兼容性规则:
|
当前持有锁 |
新请求的锁 |
能否获取 |
|
无锁 |
读锁 |
能(多个线程可同时获取) |
|
无锁 |
写锁 |
能(独占) |
|
读锁 |
读锁 |
能(共享) |
|
读锁 |
写锁 |
不能(写锁需独占,等待所有读锁释放) |
|
写锁 |
读锁 |
能(同一线程可获取,实现锁降级) |
|
写锁 |
写锁 |
能(同一线程可重入,其他线程不能) |
3.2 使用方法
ReentrantReadWriteLock的使用需分别获取读锁和写锁:
public class ReadWriteLockDemo {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock(); // 读锁
private final Lock writeLock = rwLock.writeLock(); // 写锁
private Map<String, Object> cache = new HashMap<>();
// 读操作:使用读锁
public Object get(String key) {
readLock.lock();
try {
System.out.println("读取key:" + key + ",当前线程数:" + rwLock.getReadLockCount());
return cache.get(key);
} finally {
readLock.unlock();
}
}
// 写操作:使用写锁
public void put(String key, Object value) {
writeLock.lock();
try {
System.out.println("写入key:" + key);
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
}
性能优势:在高并发读场景下,ReentrantReadWriteLock的吞吐量远高于synchronized或ReentrantLock(读操作无需互斥)。
3.3 锁降级示例
锁降级是指写锁持有者先获取读锁,再释放写锁,确保后续读操作的原子性:
public void downgradeLock() {
writeLock.lock();
try {
System.out.println("获取写锁,准备更新数据");
// 更新数据...
readLock.lock(); // 降级:获取读锁
System.out.println("获取读锁,完成降级");
} finally {
writeLock.unlock(); // 释放写锁,保留读锁
}
try {
// 持有读锁进行后续操作
System.out.println("持有读锁,读取数据");
} finally {
readLock.unlock(); // 最终释放读锁
}
}
用途:锁降级确保写操作完成后,读操作能立即看到最新数据,且不会被其他写操作中断。
四、Lock 与 synchronized 的全面对比及选择指南
4.1 功能对比
|
特性 |
Lock(以 ReentrantLock 为例) |
synchronized |
|
可重入性 |
支持 |
支持 |
|
公平性 |
可设置公平 / 非公平 |
仅非公平 |
|
锁获取方式 |
显式(lock()/unlock()) |
隐式(代码块 / 方法) |
|
可中断性 |
支持(lockInterruptibly()) |
不支持 |
|
超时获取 |
支持(tryLock(time)) |
不支持 |
|
条件变量 |
支持多条件(Condition) |
仅单条件(wait()/notify()) |
|
性能 |
高竞争场景下更优 |
低竞争场景下接近Lock |
|
灵活性 |
高(可自定义扩展) |
低(固定实现) |
4.2 适用场景选择
- 优先使用 synchronized 的场景:
- 简单的同步代码块或方法(语法简洁,不易出错);
- 无复杂需求(如中断、超时)的场景;
- 单线程或低并发场景(性能差异可忽略)。
- 优先使用 ReentrantLock 的场景:
- 需要中断等待锁的线程(如取消任务);
- 需要超时获取锁避免死锁;
- 需要多条件变量进行线程通信;
- 需要公平锁保证线程调度顺序。
- 优先使用 ReentrantReadWriteLock 的场景:
- 读操作远多于写操作的场景(如缓存、配置读取);
- 需要读写分离提高并发读性能。
五、实战案例:用 ReentrantLock 解决死锁问题
场景:两个线程分别需要获取两把锁,但获取顺序相反,使用synchronized会导致死锁,而Lock的tryLock()可避免。
解决方案:
public class DeadlockSolution {
private final Lock lockA = new ReentrantLock();
private final Lock lockB = new ReentrantLock();
// 线程1的操作:先获取lockA,再获取lockB
public void operation1() throws InterruptedException {
if (lockA.tryLock(1, TimeUnit.SECONDS)) { // 超时尝试获取lockA
try {
Thread.sleep(100); // 模拟操作
if (lockB.tryLock(1, TimeUnit.SECONDS)) { // 超时尝试获取lockB
try {
System.out.println("线程1获取到两把锁,执行操作");
} finally {
lockB.unlock();
}
} else {
System.out.println("线程1获取lockB超时,释放lockA");
}
} finally {
lockA.unlock();
}
} else {
System.out.println("线程1获取lockA超时,放弃操作");
}
}
// 线程2的操作:先获取lockB,再获取lockA
public void operation2() throws InterruptedException {
if (lockB.tryLock(1, TimeUnit.SECONDS)) { // 超时尝试获取lockB
try {
Thread.sleep(100); // 模拟操作
if (lockA.tryLock(1, TimeUnit.SECONDS)) { // 超时尝试获取lockA
try {
System.out.println("线程2获取到两把锁,执行操作");
} finally {
lockA.unlock();
}
} else {
System.out.println("线程2获取lockA超时,释放lockB");
}
} finally {
lockB.unlock();
}
} else {
System.out.println("线程2获取lockB超时,放弃操作");
}
}
public static void main(String[] args) throws InterruptedException {
DeadlockSolution solution = new DeadlockSolution();
// 启动线程1执行operation1
Thread t1 = new Thread(() -> {
try {
solution.operation1();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 启动线程2执行operation2
Thread t2 = new Thread(() -> {
try {
solution.operation2();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("操作完成");
}
}
运行结果解析:
- 线程 1 和线程 2 分别尝试获取对方已持有的锁时,会因超时机制释放已获取的锁,避免死锁;
- 输出可能为 “线程 1 获取 lockB 超时,释放 lockA” 和 “线程 2 获取到两把锁,执行操作”,或反之,具体取决于线程调度,但绝不会出现死锁。
核心原理:tryLock()的超时机制确保线程不会无限期等待锁,当获取锁失败时,会释放已持有的锁资源,打破死锁的循环等待条件。
六、总结:Lock 接口的价值与最佳实践
Lock接口及其实现类为 Java 并发编程提供了更灵活、更高效的同步选择。无论是ReentrantReadWriteLock的读写分离,还是tryLock()的超时与中断支持,都弥补了synchronized在复杂场景下的不足。
最佳实践原则:
- 当读操作远多于写操作时,优先使用ReentrantReadWriteLock提升并发性能;
- 当需要中断等待锁的线程或设置超时时间时,必须使用Lock接口;
- 始终在finally块中释放锁,避免锁泄漏;
- 简单场景下,synchronized仍是更简洁、更不易出错的选择。
理解Lock接口的设计思想,不仅能帮助我们写出更高效的并发代码,更能深入掌握 Java 并发编程的核心原理,为应对复杂场景打下坚实基础。
9万+

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



