这里就JUC包中的ReentrantReadWriteLock读写锁做相关介绍
概述
前面介绍了ReentrantLock可重入锁,但其存在明显的弊端。对于读场景而言,实际完全可以允许多个线程同时访问,而不必使用独占锁来进行并发保护。故ReentrantReadWriteLock读写锁应运而生。其内部维护了两个锁——读锁、写锁。前者为共享锁,后者则为互斥锁。具体而言,读锁可以被多个线程同时获取,而写锁只能被一个线程获取;同时读锁、写锁之间也是互斥的,即一旦某个线程获取到了读锁,则其他线程不可以同时获得写锁。反之同理。具体地,读写锁支持公平、非公平锁两种实现方式,默认为非公平锁。在锁的获取方面,其与ReentrantLock可重入锁类似。即支持lock()、lockInterruptibly()阻塞式获取,也支持tryLock()、tryLock(long timeout, TimeUnit unit)实现非阻塞式获取。但tryLock()方法会破坏公平性,即使是一个公平的读写锁实例。故为了保证公平性,可使用支持超时的tryLock方法,同时将超时时间设为0即可——tryLock(0, TimeUnit.SECONDS)。而在条件变量Condition方面,仅写锁支持
实践
读写测试
现在分别就读读、写写、读写场景进行实践验证。示例代码如下所示
1 | /** |
从Test1、Test2的测试结果可以证明读锁是共享锁、写锁是互斥锁
从Test3的测试结果可以看出读写之间是互斥的
可重入性
ReentrantReadWriteLock同样是可重入的。当一个线程获取到读锁(或写锁)后,可以继续获取相应类型的锁。示例代码如下所示
1 | /** |
测试结果,如下所示
锁升级、降级
所谓锁升级指的是读锁升级为写锁。当一个线程先获取到读锁再去申请写锁,显然ReentrantReadWriteLock是不支持的。理由也很简单,读锁是可以多个线程同时持有的。若其中的一个线程能够进行锁升级,成功获得写锁。显然与我们之前的所说的读写互斥相违背。因为其在获得写锁的同时,其他线程依然持有读锁
反之,ReentrantReadWriteLock是支持锁降级的,即写锁降级为读锁。当一个线程在获得写锁后,依然可以获得读锁。这个时候当其释放写锁,则将只持有读锁,即完成了锁降级过程。锁降级的场景也很常见,假设存在一个先写后读的方法。共计耗时5s。其中前1秒用于写操作、后4秒用于读操作。最简单的思路是对该方法从开始到结束全部使用写锁进行保护。但其实该方法后4秒的读操作完全没有必要使用写锁进行保护。因为这样会阻塞其他线程读锁的获取,效率较低。而如果通过写锁、读锁分别对前1秒、后4秒的操作进行控制,即先获取写锁、再释放写锁,然后获取读锁、再释放读锁的方案。则有可能导致并发问题,具体表现在执行该方法过程中,刚释放写锁、准备获取读锁时,其他线程恰好获取到了写锁并对数据进行了更新。而锁降级则为此场景提供了新的解决思路及方案。其一方面保证了安全,读锁在写锁释放前获取,另一方面保证了高效,因为读锁是共享的
1 | /** |
从Test 3的测试结果可以看出。由于不支持锁升级,故其在持有读锁的条件下尝试获取写锁会被一直阻塞下去
从Test 4的测试结果可以看出锁降级是可行的。这里为了便于演示,故runnable一直未释放其持有的读锁。实际应用中需要将其释放掉
实现原理
基本结构
ReentrantReadWriteLock读写锁的实现过程同样依赖于AQS,其是对AQS中共享锁、互斥锁的应用。在构建ReentrantReadWriteLock读写锁实例过程中,一方面,其会创建AQS实现类Sync的实例,其中Sync根据公平性与否又可细分为NonfairSync、FairSync这两个子类。这两个子类通过实现Sync中的readerShouldBlock、writerShouldBlock抽象方法来保障公平与否这一特性;另一方面,还会相应地创建ReadLock、WriteLock实例,并通过持有Sync实例来进行对AQS的调用。而且在ReentrantReadWriteLock读写锁的实现中,其将AQS的state字段分为两部分来使用。具体地,state字段的高16位表示获取到读锁的次数;state字段的低16位表示获取到写锁的次数
1 | public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable { |
写锁
ReentrantReadWriteLock中的写锁是对AQS中互斥锁的使用。其使用方式是通过writeLock()获取写锁实例,然后分别通过写锁的lock()、unlock()方法进行加锁、解锁操作
在调用加锁lock()方法时,其首先会调用AQS的acquire()方法。而在Sync类中则提供了tryAcquire方法的实现。如果其返回true则加锁操作结束,否则其将会进入AQS的阻塞队列。同时为了支持公平、非公平两种实现版本,Sync类中定义了writerShouldBlock抽象方法,用于判断当前线程是否可以直接通过CAS获取锁。然后通过Sync的子类NonfairSync、FairSync来实现该方法。具体地,在NonfairSync类中,writerShouldBlock方法会直接返回false。即直接利用CAS获取锁;而在FairSync类中,writerShouldBlock方法需要调用AQS的hasQueuedPredecessors方法,即如果AQS阻塞队列中如果没有其他线程在排队才可以通过CAS获取锁
类似地,在调用解锁unlock()方法时,其首先会调用AQS的release()方法。而在Sync类中则提供了tryRelease方法的实现。如果返回true则说明锁已经完全被释放了,需要AQS唤醒队列中的其他线程
1 | public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable { |
读锁
ReentrantReadWriteLock中的读锁是对AQS中共享锁的使用。其使用方式是通过readLock()获取读锁实例,然后分别通过读锁的lock()、unlock()方法进行加锁、解锁操作
在调用加锁lock()方法时,其首先会调用AQS的acquireShared()方法。而在Sync类中则提供了tryAcquireShared方法的实现。如果返回值小于0则进入AQS的阻塞队列,否则加锁操作结束。同时为了支持公平、非公平两种实现版本,Sync类中定义了readerShouldBlock抽象方法,用于判断当前线程是否可以直接通过CAS获取锁。然后通过Sync的子类NonfairSync、FairSync来实现该方法。具体地,在NonfairSync类中,readerShouldBlock方法会调用AQS的apparentlyFirstQueuedIsExclusive方法来判断AQS阻塞队列中排队的第一个节点是不是获取写锁的,如果是则放弃本次CAS操作;而在FairSync类中,readerShouldBlock方法同样需要调用AQS的hasQueuedPredecessors方法,即如果AQS阻塞队列中如果没有其他线程在排队,本次才尝试通过CAS获取锁
类似地,在调用解锁unlock()方法时,其首先会调用AQS的releaseShared()方法。而在Sync类中则提供了tryReleaseShared方法的实现。如果返回true则说明锁已经完全被释放了,需要AQS进一步唤醒队列中的其他线程
1 | public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable { |
参考文献
- Java并发编程之美 翟陆续、薛宾田著