ReentrantReadWriteLock是一个可重入读写分离锁,可以认为是在ReentrantLock上的扩展,但是二者没有继承关系,相比于ReentrantLock将读和写分离,读读不互斥,可以提高并发环境下的效率,同样也是基于AQS实现,源码比ReentrantLock要复杂一些,最好能了解AQS源码。
ReentrantReadWriteLock内部有Sync,NoNofairSync,FairSync,ReadLock,WriteLock几个内部类,这几个内部类都很关键。类似于ReentrantLock,NoNofairSync和FairSync都是继承了Sync分别实现了公平和非公平两种模式下的策略,Sync继承AQS,如下图所示。
ReadLock和WriteLock都是实现了Lock和Serializable接口,分别持有一个Sync引用,通过多态将实际的lock和unlock逻辑动态的委托给NoNofairSync和FairSync处理,而ReentrantReadWriteLock则是实现了ReadWriteLock接口,具体如下图所示。
ReentrantReadWriteLock最重要的不过就是写锁获取,读锁释放,读锁获取,读锁释放四个方法,下面对这四个方法进行详细分析,分析具体逻辑之前,先看看这个类的基本字段。
- ReentrantReadWriteLock基本字段
private final ReentrantReadWriteLock.ReadLock readerLock; //读锁引用
private final ReentrantReadWriteLock.WriteLock writerLock; //写锁引用
final Sync sync; //同步器引用
通过读锁和写锁的引用将具体的加锁,释放委托给这两个内部类
- Sync的基本字段
/**
* 读写锁共用同步状态变量,来同时维护读和写的状态,同步状态变量是一个int型变量
* 进行按位切割使用,高16位表示读的状态,低16位表示写的状态
*/
static final int SHARED_SHIFT = 16; //读锁状态偏移位,偏移16位
static final int SHARED_UNIT = (1 << SHARED_SHIFT); //读锁操作的基本单元,读锁状态+1,则状态变量值+SHARED_UNIT
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;//可重入状态的最大值
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;//写锁掩码,将同步状态变量和掩码位与就可以得出写锁的状态
还有几个基本字段需要先知道Sync的两个内部类,如下:
//用来保存一个线程读锁重入的次数
static final class HoldCounter {
int count = 0;
final long tid = getThreadId(Thread.currentThread());
}
/**
* 继承了ThreadLocal,通过ThreadLocal实现记录线程获取读锁次数的记录
* key是Thread对象的引用,value是HoldCounter对象
* HoldCounter对象中保存了获取读锁次数count,和线程tid
*/
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
private transient ThreadLocalHoldCounter readHolds;//保存线程获取读锁的次数
/**
* 最后一个成功获取读锁的线程对应的HoldCounter对象引用,是一个缓存
* 当获取和释放读锁的时候,需要更新HoldCount,会先检测缓存的线程ID是否和当前线程ID相同
* 如果相同,就直接通过缓存引用来更新HoldCount值
* 否则,重新从readHolds中获取HoldCounter对象,同时更新这个缓存
*/
private transient HoldCounter cachedHoldCounter;
private transient Thread firstReader = null; //第一个获取读锁的线程
private transient int firstReaderHoldCount; //firstReader获取了多少次读锁
以上几个字段主要用于提升效率,如只有一个线程的时候,只会操作firstReaderHoldCount这个整型,以及防止写锁饥饿等
- 写锁的获取,因为ReentrantLock获取的就是独占锁,所以写锁的获取和释放基本和ReentrantLock相差不大
public void lock() {
sync.acquire(1);
}
委托给sync对象引用,直接调用了AQS的acquire方法,由AQS的源码可知,这里的调用会导向tryAcquire方法,如下:
/**
* 独占锁(写锁)释放
*/
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) { //说明这个锁已经被某个线程持有,可能是读或写
// (Note: if c != 0 and w == 0 then shared count != 0)
/**
* c!=0 && w==0表示有线程持有共享锁,就算是自己持有共享锁,也会阻塞
* 所以ReentrantReadWrLock不支持锁升级
* current != getExclusiveOwnerThread()表示,有其他线程持有独占锁
* 这两种情况都需要阻塞自己
*/
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//到这里说明是线程进行了重复获取锁,所以进行可重入处理,直接返回
setState(c + acquires);
return true;
}
/**
* 程序执行到这里说明是这个锁是首次被获取,所以要进行并发处理,使用CAS.
* 1.如果是公平锁,那么writerShouldBlock只允许队列头的线程获取锁
* 2.如果是非公平锁,不做限制,writerShouldBlock直接返回false
*/
//
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
这里使用了writerShouldBlock这个方法来限制获取锁的策略,这个方法是Sync提供的Abstract方法,然后由NoNofairSync和FailSync分别实现不同的策略。
FairSync的writerShouldBlock实现:
/**
* 对于公平锁来说,如果有前驱(也就是非头结点),都会进行等待,不允许竞争锁
*/
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
NoNofairSync的writerShouldBlock实现:
final boolean writerShouldBlock() {
return false; // 对于非公平锁,没有任何限制
}
- 写锁的释放
public void unlock() {
sync.release(1);
}
同理委托给Sync处理,Sync调用AQS的release方法,会导向tryRelease方法,Sync重写如下:
/**
* 独占锁(写锁)获取
* 与ReentrantLock中的tryRelease方法没有什么差别
*/
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively()) //如果当前线程不是持有锁的独占线程,抛出异常
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)//因为可重入,只有当exclusiveCount(nextc) == 0时候,才会真正释放锁
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
- 读锁的获取,读锁的获取比较复杂
public void lock() {
sync.acquireShared(1);
}
同理委托给Sync处理,Sync调用AQS的acquireShared方法,会导向tryAcquireShared方法,Sync重写如下:
/**
* 共享锁(写锁)获取
* @param unused
* @return
*/
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
/**
* 已经有其他的线程持有了独占锁,让自己阻塞
* 但是如果是自己已经持有了独占锁,那么允许自己再获取共享锁
* ReentrantReadWriteLock的支持锁降级,但是不支持锁升级
*/
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
/**
* 1.通过公平/非公平的排队规则限制
* 2.读锁状态未达到可重入状态的最大值
* 3.CAS设置同步状态成功
*/
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) { //第一次被线程获取
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) { //更新首次获取锁线程HoldCount
firstReaderHoldCount++;
} else { //更新非首次获取锁线程的HoldCount
HoldCounter rh = cachedHoldCounter; //先查缓存
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get(); //缓存没有命中,从ThreadLocal中获取
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
//一次获取读锁失败后,尝试循环获取
return fullTryAcquireShared(current);
}
这个方法里调用了几个子方法,首先来看readerShouldBlock方法,同样这个也是分别有公平和非公平两种实现
FairSync的readerShouldBlock实现:
/**
* 对于公平锁来说,如果有前驱(也就是非头结点),都会进行等待,不允许竞争锁
*/
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
NoNofairSync的readerShouldBlock实现:
final boolean readerShouldBlock() {
/**
* 由于非公平的竞争,并且读锁可以共享,所以可能会出现源源不断的读
* 使得写锁永远竞争不到,然后出现饿死的现象
* 所以通过这个策略,当一个写锁申请线程出现在头结点后面的时候
* 会立刻阻塞所有还未获取读锁的其他线程,让步给写线程先执行
*/
return apparentlyFirstQueuedIsExclusive();
}
在tryAcquireShared中经行了一次快速锁获取,但是由于CAS只能允许一个线程获取锁成功,且读锁是共享的,可能存在其他仍然可以获取锁的线程,所以在函数末尾调用函数fullTryAcquireShared来进行死循环的获取锁,这个函数很关键,代码分析如下:
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
/**
* 如果是其他线程获取了写锁,那么把当前线程阻塞
* 如果是当前线程获取了写锁,不阻塞,否则会造成死锁
* 从这里可以看到ReentrantReadWriteLock允许锁降级
*/
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {
/**
* 进入这里说明,同步队列的头结点的后继有一个竞争写锁的线程
* 所以这里有一个锁让步的操作,即让写锁先获取
* 如果是firstReader必然是重入,或者rh.count>0也必然是重入
* 对于读锁重入是允许死循环直到获取锁成功的,不然会导致死锁
* 但是如果rh.count = 0就说明,这个线程是第一次获取读锁
* 为了防止写饥饿,直接将他们重新扔会同步队列,而且这些阻塞不会导致死锁
*/
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
- 读锁的释放
public void unlock() {
sync.releaseShared(1);
}
同理委托给Sync处理,Sync调用AQS的releaseShared方法,会导向tryReleaseShared方法,Sync重写如下:
/**
* 共享锁(读锁)释放
*/
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) { //更新第一个获取到读锁线程的HoldCount
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else { //更新其他线程的HoldCount
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) { //如果count <= 1就清除这个key,value
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) { //死循环CAS更新state
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
学习完ReentrantReadWriteLock源码,应该了解如何通过AQS实现读写分离,如何实现锁降级,如何防止非公平模式下写线程饿死等这些问题。