ReentrantReadWriteLock 源码讲解
引言
ReentrantLock 和ReentrantReadWriteLock 可以说是双生兄弟,前置是一个独占锁,后者是一个共享锁 ReentrantLock源码讲解
知识点1. 继承AQS的类都需要使用state变量代表某种资源,在ReentrantReadWriteLock中state的高16位代表读锁的个数;低16位代表写锁的状态。
1、ReentrantReadWriteLock中的读锁和写锁的实现方式
ReentrantReadWriteLock的源码中又两个重要的属性,如下图,
再来看它的构造方法,可以看到ReentrantReadWriteLock也分为公平锁和非公平锁,默认为非公平锁。和ReentrantLock一样都是通过Sync内部类实现。
他们两个分别为两个静态内部类对他们的实现
abstract static class Sync extends AbstractQueuedSynchronizer {}
public static class ReadLock implements Lock, java.io.Serializable {
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquireShared(1); //共享
}
public void unlock() {
sync.releaseShared(1); //共享
}
}
public static class WriteLock implements Lock, java.io.Serializable {
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquire(1); //独占
}
public void unlock() {
sync.release(1); //独占
}
}
复制代码
到现在可以看到,ReentrantLock 和ReentrantReadWriteLock两兄弟都是基于AQS来实现的,不同的是:WriteLock和ReentrantLock一样,使用了独占锁。而ReadLock和Semaphore一样,使用了共享锁。
2、ReentrantReadWriteLock的公平锁和非公平锁源码
通过上图我们可以看出在ReentrantReadWriteLock中主要是通过他们共同的父类Sync来实现的。再看Sync之前,先介绍一下readerShouldBlock
和writerShouldBlock
的作用。
writerShouldBlock
和readerShouldBlock
方法都表示当有别的线程也在尝试获取锁时,是否应该阻塞。
其中公平锁的hasQueuedPredecessors
是我们老朋友,在ReentrantLock
中的公平锁中我们见过他的身影,它的作用是用来判断当前的线程是不是队列当中的第一个,是第一个则返回ture,不是第一个则返回false。
非公平模式下,writerShouldBlock直接返回false,说明不需要阻塞;而readShouldBlock调用了apparentFirstQueuedIsExcluisve()方法。该方法在当前线程是写锁占用的线程时,返回true;否则返回false。也就说明,如果当前有一个写线程正在写,那么该读线程应该阻塞。
3、读锁的获取
先查看读锁的获取流程
锁的获取看下图。当tryAcquireShared()方法小于0时,那么会执行doAcquireShared方法将该线程加入到等待队列中。
Sync实现了tryAcquireShared方法,如下:
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 如果当前有写线程并且本线程不是写线程,不符合重入,失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 得到读锁的个数
int r = sharedCount(c);
// 1.判断是否应该阻塞
// 公平锁和非公平锁判断阻塞的方式不一样、具体查看公平锁和非公平锁源码
// 2.判断读锁的个数是不是小于65535
// 3.CAS更新读锁的值
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
// 如果当前读锁为0做初始化的操作
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
// 因为是可重入锁,所以当冲入的时候读的计数器+1
} else if (firstReader == current) {
firstReaderHoldCount++;
// 当前读线程和第一个读线程不同,记录每一个线程读的次数
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
// 否则,循环尝试 什么时候会循环尝试换取
// 1.公平锁的且有其他线程在排队时
// 2.读锁的个数>=65535时,需等待其他释放
// 3.CAS失败时
return fullTryAcquireShared(current);
}
复制代码
cachedHoldCounter
是最后一个获取到读锁的线程计数器,每当有新的线程获取到读锁,这个变量都会更新。这个变量的目的是:当最后一个获取读锁的线程重复获取读锁,或者释放读锁,就会直接使用这个变量,速度更快,相当于缓存。并且每一个线程没都有自己的计数器。
fullTryAcquireShared方法如下
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
//一旦有别的线程获得了写锁,返回-1,失败
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// 如果读线程需要阻塞
} else if (readerShouldBlock()) {
if (firstReader == current) {
// 说明有别的读线程占有了锁
} 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;
}
}
// 如果读锁达到了最大值,抛出异常
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 如果成功更改状态,成功返回
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 获取成功之后,当读线程和第一个读线程不同,记录每一个线程读的次数
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
复制代码
可以看到fullTryAcquireShared与tryAcquireShared有很多类似的地方。
在上面可以看到多次调用了readerShouldBlock方法,对于公平锁,只要队列中有线程在等待,那么将会返回true,也就意味着读线程需要阻塞;对于非公平锁,如果当前有线程获取了写锁,则返回true。一旦不阻塞,那么读线程将会有机会获得读锁。
4、写锁的获取
写锁的lock方法如下: 从上面可以看到,写锁使用的是AQS的独占模式。首先尝试获取锁,如果获取失败,那么将会把该线程加入到等待队列中。
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
// 得到写锁的个数
int w = exclusiveCount(c);
// 如果当前有写锁或者读锁
if (c != 0) {
// 写锁为0或者但当前线程等于不是独占的线程获取锁失败
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失败,返回false
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
// 否则将当前线程置为获得写锁的线程,返回true
setExclusiveOwnerThread(current);
return true;
}
复制代码
获取锁失败加入等待队列,请查看我上一篇文章ReentrantLock
小结
如果当前没有写锁或读锁时,第一个获取锁的线程都会成功,无论该锁是写锁还是读锁。
如果当前已经有了读锁,那么这时获取写锁将失败,获取读锁有可能成功也有可能失败。
如果当前已经有了写锁,那么这时获取读锁或写锁,如果线程相同(可重入),那么成功;否则失败。
5、读锁的释放
话不多说,上图
doReleaseShared() 是释放成功之后去唤醒下一个节点
tryReleaseShared()源码的具体实现如下
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 如果是第一个获得读锁的线程
if (firstReader == current) {
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
}
// 当前线程的计数器-1
else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != 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();
//释放一把读锁 因为高16位代表读锁,所以-65536
int nextc = c - SHARED_UNIT;
// 更新成功返回,否者尝试
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
复制代码
6、写锁的释放
tryReleasevi释放写锁,释放完成之后,如果队列中还有其他的线程那么需要去唤醒堵塞的线程。
protected final boolean tryRelease(int releases) {
// 如果没有线程持有写锁,但是仍要释放,抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
// 读锁是否全部释放
boolean free = exclusiveCount(nextc) == 0;
// 如果没有写锁了,那么将AQS的线程置为null
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
复制代码
小结
从上面得源码可以看出:
1、如果当前是写锁被占有了,只有当写锁的数据降为0时才认为释放成功;否则失败。因为只要有写锁,那么除了占有写锁的那个线程,其他线程即不可以获得读锁,也不能获得写锁。
2、如果当前是读锁被占有了,那么只有在写锁的个数为0时才认为释放成功。因为一旦有写锁,别的任何线程都不应该再获得读锁了,除了获得写锁的那个线程。