Java ReentrantReadWriteLock的锁降级机制实现
在Java并发编程中,ReentrantReadWriteLock 提供了一种比互斥锁更灵活的锁机制,允许多个线程同时读取,但写入时独占。锁降级是一种重要的优化策略,它指的是持有写锁的线程在释放写锁之前,先获取读锁,随后再释放写锁的过程。这一机制保证了当前线程在修改数据后,能够立即读取到最新的数据,并且在读锁释放前,其他线程无法进行修改,从而确保了数据可见性。
以下是实现和理解锁降级的完整步骤。
1. 理解锁降级的执行流程
锁降级不是随意的锁切换,必须遵循严格的顺序,否则会导致死锁或数据不一致。核心流程可以概括为:先获取写锁,修改数据,再获取读锁,最后释放写锁。
为了直观展示这一过程,请参考以下流程图:
注意流程中的顺序:获取读锁的动作必须发生在释放写锁之前。如果先释放写锁,其他线程可能会立即获取写锁并修改数据,导致当前线程后续的读取操作无法看到自己刚才的修改,破坏了数据的一致性。
2. 编写锁降级的核心代码
为了演示这一机制,我们需要构建一个包含读写锁的数据缓存类。请按照以下步骤编写代码。
- 创建一个名为
CachedData的Java类。 - 定义两个关键成员变量:一个
Object类型的data用于存储数据,一个ReentrantReadWriteLock类型的rwLock用于控制并发。 - 初始化锁对象,并分别提取出读锁和写锁。
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class CachedData {
private Object data;
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
// 其他方法将在此处实现
}
3. 实现锁降级的具体逻辑
在 CachedData 类中,我们需要添加一个方法来处理数据更新并执行锁降级。请执行以下步骤:
- 定义一个方法
processAndUpdateData。 - 调用
writeLock.lock()获取写锁,确保独占访问。 - 执行数据修改逻辑(此处模拟简单的赋值)。
- 调用
readLock.lock()在写锁未释放的情况下获取读锁。这是降级的关键一步。 - 调用
writeLock.unlock()释放写锁。此时线程依然持有读锁。 - 执行数据读取逻辑,验证数据一致性。
- 调用
readLock.unlock()释放读锁。
public void processAndUpdateData(Object newData) {
// 步骤2: 获取写锁
writeLock.lock();
try {
// 步骤3: 修改数据
this.data = newData;
// 步骤4: 锁降级关键:在持有写锁时获取读锁
// 这保证了当前线程在释放写锁前,对数据的“读”权限已经预定
readLock.lock();
} finally {
// 步骤5: 释放写锁,此时线程降级为“读模式”
// 其他线程可以获取读锁,但无法获取写锁
writeLock.unlock();
}
try {
// 步骤6: 使用读锁读取数据
// 在这里,data 必然是刚才修改后的 newData,因为写锁释放后,其他写线程被阻塞
System.out.println("Data after downgrade: " + this.data);
// 模拟耗时的读操作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 步骤7: 释放读锁
readLock.unlock();
}
}
4. 区分锁升级与锁降级
理解锁降级时,必须明确区分它与“锁升级”的区别。Java 的 ReentrantReadWriteLock 不支持锁升级。
| 特性 | 锁降级 | 锁升级 |
|---|---|---|
| 定义 | 写锁 -> 读锁 | 读锁 -> 写锁 |
| 支持情况 | 支持 | 不支持 |
| 操作顺序 | 持有写锁时获取读锁,然后释放写锁 | 持有读锁时获取写锁 |
| 后果 | 保证数据修改后的可见性 | 会导致死锁 |
严禁在持有读锁的情况下尝试获取写锁。因为两个或多个线程同时持有读锁并都想升级为写锁时,它们会互相等待对方释放读锁,从而永远阻塞。
5. 验证锁降级的实际效果
为了确保上述代码正确运行,我们可以编写一个 main 方法进行测试。
- 实例化
CachedData对象。 - 启动多个线程,其中一个调用更新方法,其他尝试读取。
- 观察控制台输出,确认在更新线程进行锁降级期间(持有读锁期间),其他读线程可以并发访问,但写线程被阻塞。
public static void main(String[] args) {
CachedData cache = new CachedData();
// 线程1:执行更新和锁降级
new Thread(() -> {
cache.processAndUpdateData("New Version Data");
}).start();
// 线程2:尝试读取(可以并发执行)
new Thread(() -> {
cache.readData();
}).start();
}
public void readData() {
readLock.lock();
try {
System.out.println("Reading data: " + this.data);
} finally {
readLock.unlock();
}
}
在上述代码中,当线程1执行锁降级并进入 sleep 状态时,它仍然持有读锁。此时,线程2也可以获取读锁并读取数据,两个读线程共存。而任何试图获取写锁的线程将会被阻塞,直到线程1完全释放读锁。这证实了锁降级既保证了数据对当前线程的可见性,又利用了读锁的共享特性提高了并发效率。

暂无评论,快来抢沙发吧!