Java并发编程中锁是一种用于同步访问共享资源的机制。Java并发库(JUC)提供了多种锁的实现,每种都有其特定的使用场景和优势。其中,ReentrantReadWriteLock是一种非常独特且实用的锁,它允许更高的并发性,特别适用于读多写少的场景。
一、ReentrantReadWriteLock的基本概念
ReentrantReadWriteLock,即可重入的读写锁,它维护了两把锁:读锁和写锁。读锁允许多个线程同时持有,从而允许多个线程同时读取共享资源,提高了并发读取的效率。而写锁则是独占的,同一时间只能被一个线程持有,用于保护写入共享资源的操作。
要注意的是,读锁和写锁是互斥的。当写锁被持有时,其他线程无法获取读锁或写锁,从而保证了写入操作的独占性。而当读锁被持有时,虽然其他线程可以继续获取读锁,但无法获取写锁,这保证了在读取过程中共享资源不会被修改。
二、ReentrantReadWriteLock的特性
1. 可重入性:ReentrantReadWriteLock允许同一个线程多次获取同一个锁,无论是读锁还是写锁。这使得线程可以在持有锁的情况下进行递归调用或多次访问共享资源,而不会产生死锁。
2. 公平性选择:ReentrantReadWriteLock可以在创建时选择是否是公平的。公平锁意味着锁的获取顺序将按照线程请求锁的顺序来,即遵循先来先服务的原则;而非公平锁则不保证按照顺序分配锁,可能会导致某些线程长时间得不到锁。
3. 锁降级:ReentrantReadWriteLock支持锁降级操作,即线程可以先获取写锁,然后释放写锁并获取读锁。这在某些场景下非常有用,比如线程在修改共享资源后需要读取修改后的结果。
三、ReentrantReadWriteLock原理解读
ReentrantReadWriteLock的实现机制相对复杂,涉及多个内部类和同步控制。
3.1 核心组件
1. Sync:ReentrantReadWriteLock内部的一个抽象队列同步器(AQS),它继承自AbstractQueuedSynchronizer,用于实现锁的核心逻辑。
2. ReadLock:读锁的实现,内部持有一个Sync对象,用于实现读锁的获取和释放。
3. WriteLock:写锁的实现,同样内部持有一个Sync对象,用于实现写锁的获取和释放。
3.2 实现机制
• 状态表示:Sync内部使用一个整型的state字段来表示锁的状态。高16位表示读锁的持有次数,低16位表示写锁的持有次数。
• 写锁获取:当线程尝试获取写锁时,会调用Sync的tryAcquire方法。如果state不为0(即已经有读锁或写锁被持有),则获取失败。如果成功获取写锁,会将state的低16位加1,并设置当前线程为锁的持有者。
• 读锁获取:当线程尝试获取读锁时,会调用Sync的trySharedAcquire方法。如果state的低16位不为0(即有写锁被持有),则获取失败。如果成功获取读锁,会将state的高16位加1,并可能设置当前线程为锁的持有者(如果是第一个获取读锁的线程)。
• 锁释放:无论是读锁还是写锁,释放时都会调用Sync的tryRelease或trySharedRelease方法。释放写锁时,会将state的低16位减1;释放读锁时,会将state的高16位减1。如果完全释放了锁(state变为0),则会唤醒等待队列中的其他线程。
• 公平性:ReentrantReadWriteLock在构造时可以指定是否为公平锁。公平锁会按照线程请求锁的顺序来分配锁,而非公平锁则可能会跳过等待队列中的某些线程直接获取锁。
3.3 源码骨架分析
ReentrantReadWriteLock的源码较长,给出部分关键代码分析。
// ReentrantReadWriteLock的部分内部类定义
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable{
// 内部抽象类Sync继承自AbstractQueuedSynchronizer
abstract static class Sync extends AbstractQueuedSynchronizer{
// ... 省略其他方法和字段 ...
// 尝试获取写锁的方法(由WriteLock调用)
final boolean tryAcquire(int acquires){
}
// 尝试释放写锁的方法(由WriteLock调用)
final boolean tryRelease(int releases){
}
// 尝试获取读锁的方法(由ReadLock调用)
final int tryAcquireShared(int acquires){
}
// 尝试释放读锁的方法(由ReadLock调用)
final boolean tryReleaseShared(int releases){
}
// 判断是否处于读锁状态
final boolean isHeldExclusively(){
}
}
// 非公平锁的实现类NonfairSync继承自Sync
static final class NonfairSync extends Sync{
}
// 公平锁的实现类FairSync继承自Sync
static final class FairSync extends Sync{
}
// 读锁的实现类ReadLock实现Lock接口
public static class ReadLock implements Lock, java.io.Serializable{
// 读锁的lock方法实现
publicvoidlock(){
sync.acquireShared(1);
}
// 读锁的unlock方法实现
publicvoidunlock(){
sync.releaseShared(1);
}
}
// 写锁的实现类WriteLock实现Lock接口
public static class WriteLock implements Lock, java.io.Serializable{
// 写锁的lock方法实现
public void lock(){
sync.acquire(1);
}
// 写锁的unlock方法实现
public void unlock(){
sync.release(1);
}}
// ... 省略其他方法和字段 ...
}
ReentrantReadWriteLock定义了两个内部类ReadLock和WriteLock来实现读锁和写锁的功能。它们内部都持有一个Sync对象,用于实际的锁操作。Sync是一个继承自AbstractQueuedSynchronizer的抽象类,它实现了锁的核心逻辑。具体的公平性和非公平性锁的实现则通过FairSync和NonfairSync两个类来完成,它们分别继承自Sync。
四、ReentrantReadWriteLock的使用
使用ReentrantReadWriteLock
的代码,包含一个共享资源(一个简单的计数器),多个读线程和写线程将并发地访问这个资源。读线程只会读取计数器的值,而写线程会修改计数器的值。
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample{
// 共享资源:一个简单的计数器
private int counter=0;
// 创建一个ReentrantReadWriteLock对象
private final ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
// 获取读锁
private final ReentrantReadWriteLock.ReadLock readLock= readWriteLock.readLock();
// 获取写锁
private final ReentrantReadWriteLock.WriteLock writeLock= readWriteLock.writeLock();
// 读操作:增加读计数
public int readCounter(){
readLock.lock();// 加读锁
try {
// 模拟读操作的耗时
Thread.sleep(10);
return counter;
}catch(InterruptedException e){
e.printStackTrace();
return-1;
}finally{
readLock.unlock();// 释放读锁
}
}
// 写操作:增加计数器的值
public void incrementCounter(){
writeLock.lock();// 加写锁
try{
// 模拟写操作的耗时
Thread.sleep(10);
counter++;
}catch(InterruptedException e){
e.printStackTrace();
}finally{
writeLock.unlock();// 释放写锁
}
}
// 读线程类
classReaderThreadextendsThread{
@Override
public void run(){
for(inti=0; i <5; i++){
System.out.println("读线程"+Thread.currentThread().getId()+"读取计数器值:"+ readCounter());
}
}
}
// 写线程类
class WriterThread extends Thread{
@Override
public void run(){
for(inti=0; i <5; i++){
incrementCounter();
System.out.println("写线程"+Thread.currentThread().getId()+"增加计数器值");
}
}
}
// 测试方法
public static void main(String[] args){
ReadWriteLockExampleexample=newReadWriteLockExample();
// 启动多个读线程和写线程
for(inti=0; i <3; i++){
newThread(example.newReaderThread()).start();
newThread(example.newWriterThread()).start();
}
}
}
ReadWriteLockExample
包含一个计数器counter
,一个ReentrantReadWriteLock
对象以及对应的读锁和写锁。readCounter
和incrementCounter
分别执行读操作和写操作,并在执行前后分别加锁和释放锁。
五、ReentrantReadWriteLock注意事项
1. 避免锁升级:与锁降级相反,锁升级(从读锁升级到写锁)是不被支持的。如果线程已经持有了读锁并试图获取写锁,将会导致死锁。因此,在设计并发程序时应尽量避免这种情况的发生。
2. 注意读写锁的互斥性:虽然多个线程可以同时持有读锁,但写锁是独占的。在设计并发程序时,应充分考虑读写锁的互斥性,避免因为不恰当的锁使用导致并发性能下降或死锁等问题。
3. 选择合适的公平性策略:根据实际需求选择合适的公平性策略是非常重要的。公平锁可以确保所有线程都有机会获取锁,避免了某些线程长时间得不到锁的情况;但非公平锁在某些场景下可能具有更高的并发性能。
太强 ! SpringBoot中出入参增强的5种方法 : 加解密、脱敏、格式转换、时间时区处理
太强 ! SpringBoot中优化if-else语句的七种绝佳方法实战
提升编程效率的利器: Google Guava库之RateLimiter优雅限流
SpringBoot中大量数据导出方案:使用EasyExcel并行导出多个excel文件并压缩zip后下载
Elasticsearch揭秘:高效写入与精准检索的流程原理全解析
Spring Boot中Druid连接池与多数据源切换的方案
【Elasticsearch系列】深入解析Elasticsearch中脚本原理