上回说到ReentrantLock,今天来谈谈读写锁(ReentrantLock)和其具体实现ReentrantReadWriteLock。看这篇文章前,强烈建议你回到先读懂ReentrantLock,因为ReentrantReadWriteLock其实是在ReentrantLock的基础上实现的,可以参考我之前的博客ReentrantLock源码解析
既然有了锁,为什么还需要读写锁?我们来想象下这个场景。你们小区楼下有个公告栏,有时候有人会写个招租,有时候有人会写个寻物启事…… 当然一个人正在改公告栏的时候,另外一个人就不能同时改了,这里就相当于有了一把无形的锁,我改的时候就把广告栏“锁住”,改完再“解锁”,当然别人锁住了之后我也改不了。说完了“写”再说“读”,一个人在读公告栏的时候,别人就不能去写了,这样不礼貌,这里也相当于读的人用一把“锁”把公告栏给锁了。
如果这里读者用的锁和写者用的锁是一样的,那么这把锁不紧不然别人写了,也不让别人读了,相当于一个人在看公告栏,别人就不能看了,这明显不合理啊。 所以要把读和写用的锁区分开来,所有读的人共享一把锁,写的人独享锁。放到公告栏的例子上,改公告的时候同时只有一个人可以看,但读的时候所有人可以同时读,这样就可以把“公告栏”这个资源的利用率最大化。
看到这里,你应该已经理解了什么叫做“读写锁”,接下来我们直接看下jdk中ReentrantReadWriteLock的实现,再次建议先阅读ReentrantLock的具体实现。
从类结构图看,貌似它比ReentrantLock更复杂写,多两个内部类 ReadLock 和 WriteLock,看着Lock提供的api完全一样,看来得从具体实现上来看其二者有什么样的差异了。
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
从ReentrantReadWriteLock的构造方法可以看出,它也支持公平锁和非公平锁,当然默认也是非公平锁。和ReentrantLock一样,加锁和解锁的实现逻辑都是在 Sync 里,所以我们重点看下Sync的实现,代码太多这里就不贴完整代码了,建议读者自行打开代码。
Sync
从Sync的类结构图来看,它还是相当复杂的,别急让我们来捋一捋,我们先从WriteLock看起(看起来会比较熟悉),看下他的lock和release的具体实现。
@ReservedStackAccess
final boolean tryWriteLock() {
Thread current = Thread.currentThread(); // 1
int c = getState(); // 2
if (c != 0) { // 3
int w = exclusiveCount(c); // 4
if (w == 0 || current != getExclusiveOwnerThread()) // 5
return false;
if (w == MAX_COUNT) //6. MAX_COUNT = 65535
throw new Error("Maximum lock count exceeded");
}
if (!compareAndSetState(c, c + 1)) // 7
return false;
setExclusiveOwnerThread(current); // 8
return true;
}
如果你看过ReentrantLock的话,相信这段代码你已经完全能看懂了。这里我再大概说下这段代码的流程
- 获取到当前线程。
- 获取到锁对象的state值,state是保存了锁的状态。
- 如果state不为0,说明已经有线程加过锁了,这时候需要额外判断下,跳到4。 如果state为0,直接跳到 7。
- 获取到当前加写锁的次数,这里获取的是state的低16位。
- c已经不为0了,如果w不为0说明有线程加了写锁,如果加了写锁的线程也不是当前线程的,加锁就失败了。
- 这里需要额外判断下锁重入的次数,如果已经到65535就不能再加锁了,后续会解释为什么是65535。
- 执行CAS操作更改锁状态 state。
- 到这里说明加写锁已经成功了,把当前锁的持有者记录下来。
@ReservedStackAccess
final boolean tryReadLock() {
Thread current = Thread.currentThread(); // 1
for (;;) {
int c = getState(); // 2
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current) // 3
return false;
int r = sharedCount(c); // 4
if (r == MAX_COUNT) // 5
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) { // 6
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null ||
rh.tid != LockSupport.getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return true;
}
}
}
读锁的加锁代码就完全不一样了,第一眼看到的不同就是这里有个大大的无限循环,我们还是来看下读锁的加锁过程。
- 获取当前线程。
- 获取锁的state状态值。
- 如果写锁的加锁次数不是0切写锁持有者不是当前线程,加读锁失败。
- 获取读锁的加锁次数,sharedCount(c)获取的是state的高16位。
- 如果读锁加锁次数达到65535,抛Error,和写锁一样,只能加65535次。
- 执行到这,说明可以加锁,使用CAS更新state成功后这里就开始记录一些读锁的状态信息,注意这里state增加值不是1,而是SHARED_UNIT(65536)。
看完readLock和writeLock的加锁方式就可以大体理解ReentrantReadWriteLock的实现了,原来它只是把ReentrantLock中的state分成两部分来用,高16位记录读锁状态,低16位记录写锁状态,如下图。
这也是为什么上文中加锁最大次数是65535的原因了,这也是而是SHARED_UNIT的值为65536的原因。
理解了加锁的代码,解锁部分也就好理解了,本质上是把加锁的代码反向执行下,代码如下。
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
@ReservedStackAccess
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null ||
rh.tid != LockSupport.getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
Sync中还有一个ThreadLocalHoldCounter类,这个类的作用其实是记录每个线程对读锁的加锁测试,见名知意线程级的统计,代码也很简单,这里就不再贴了。
Sync中除了上文说到的几个加解锁的API,其余一些API就是获取Sync对象中各个状态的API,没什么好说的。
FairSync & NonfairSync
说完了抽象类Sync,我们来说下它的两个具体实现 FairSync 和 NonfairSync。 这两个实现类非常非常简单,只是重写了 writerShouldBlock() 和 readerShouldBlock() 方法而已,如果你已经知道什么是公平和非公平了,这地方也就很好理解了。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
// 写锁可以始终不被等待队列里的线程阻塞,只要当前锁是未锁定状态就可以加锁
return false;
}
final boolean readerShouldBlock() {
//这个方法判断队列的head.next是否正在等待写锁,这个方法确保读锁不应该让写锁始终等待,即便是非公平的,但写锁有更高的优先级,获取读锁还是得排队。
return apparentlyFirstQueuedIsExclusive();
}
}
// 公平锁就很好理解了,只要等待队列不为空,就得去排队
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
ReadLock & WriteLock
其实看完Sync里的逻辑,基本上ReadLock和WriteLock的实现逻辑我们已经知道了。ReadLock和WriteLock只是向用户提供里有些功能抽象(实现了Lock中的方法),封装好了具体的实现,其实具体逻辑还是在Sync中实现。
从类继承关系来看,二者也只是简单
结论
了解完ReentrantReadWriteLock的实现后你就会发现,它其实和ReentrantLock一样,之前把ReentrantLock中的state切分成两部分用,高16位作为读锁的state,低16位作为写锁。如果把ReadLock和WriteLock拉出来单独看的话,二者都是一个ReentrantLock,只是不能像ReentrantLock那样重入那么多次而已。
ReentrantReadWriteLock的出现大幅提升了多读少写场景下的性能问题,但它依旧有自己的缺点,就是它可能会导致写饥饿。还是拿小区公告栏的例子,如果任意时刻都有人在看公告栏,你也不好打断人家所以你公告更新不了啊,所以想更新的人就得一直等着。
关注我,下次和大家一起看下 StampedLock 是如何解决饥饿问题的。