释放读锁
读锁的释放其实主要还是对state进行操作,只不过因为共享锁和独占锁都使用state存储锁的数量,所以需要在释放读锁时修改state的高16位的值。
// ReentrantReadWriteLock
public void unlock() {
sync.releaseShared(1);
}
// AQS
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// ReentrantReadWriteLock.Sync
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 != 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))
return nextc == 0;
}
}
释放读锁最终调用的是tryReleaseShared方法,关于firstReader、firstReaderHoldCount等属性的含义可以参考Java并发编程之ReentrantReadWriteLock番外篇
这里就不再过多赘述。
- 首先判断当前线程是否是第一个获取读锁的线程,如果是的话判断持有锁数量是否是
1,是的话表示释放锁之后当前线程就不再持有锁了,所以就把firstReader置为null,如果重入数量不是1,就对firstReaderHoldCount减1表示减少一次重入。 - 首先判断最后一个获取成功读锁的线程是否是当前线程,如果是的话就直接通过缓存的数据对锁数量减
1,这样比线程本地获取锁数量更高效,如果当前线程不是最后一个线程那就只能到线程本地去获取锁数量,然后再减1。而如果当前线程持有锁数量为1,表示释放锁之后就不再持有锁,所以就从线程本地把记录持有锁数量的对象移除掉,防止出现内存泄漏。 - 修改完线程本身持有锁的数量之后,还要修改总的读锁的数量,也就是修改
state的高16位,一直重试到修改成功为止。
释放锁成功之后又调用了doReleaseShared方法来唤醒后继节点,因为多个读锁是可以共享的,所以一个读锁释放后可以唤醒后续所有连续的读锁节点。
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
这里依然是判断头节点是否是Node.SIGNAL状态,是的话就来唤醒后继节点。因为head节点是在后继节点进行阻塞是把waitStatus修改为Node.SIGNAL的,目的就是为了能够唤醒后继节点,如果获取到的值是0的话可能是后继节点还没有执行到修改head节点的waitStatus,这里尝试修改从0修改为Node.PROPAGATE,如果失败了表示已经不是0了,就可能是SIGNAL了,所以继续重试尝试唤醒后继节点。