建议
阅读本篇前最好先阅读
并发编程源码解析(一)ReentrantLock源码解析(超详细)-优快云博客
并发编程源码解析(二)ReentrantReadWriteLock源码解析之一写锁-优快云博客
一、为什么要有读写锁?
读取操作远远多于写入操作的场景,读取的操作没必要也锁定资源, 就使用读写锁可以让多个线程同时读取同一份数据共享锁资源,以提高并发效率。
二、读锁源码解析
2.1 锁
2.1.1 acquireShared 方法
因为其他方法没什么好介绍的咱直接介绍acquireShared方法。
写锁因为共享,所以高16位只储存进入锁的线程数,如果说读锁重入那么重入次数是记录在ThreadLocal当中自行维护,详见readHolds.get() 方法。
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
protected final int tryAcquireShared(int unused) {
// 获取主线程 和 state 状态
Thread current = Thread.currentThread();
int c = getState();
// c & EXCLUSIVE_MASK 方法,这样可以得到写锁的持锁状态
// 回忆一下蒙板参数为 (1 << 16) - 1
// 0000 0000 0000 0000 1111 1111 1111 1111
// 如果存在写锁,写读可重入,所以如果是持有锁的线程是可重入,不用结束方法。
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// return c >>> 16, 右移16位只剩下高位的16位,即只剩下读锁
// 如: 0000 0000 0000 0001 0000 0000 0000 0001 右移后
// 0000 0000 0000 0000 0000 0000 0000 0001
int r = sharedCount(c);
// readerShouldBlock 主要的功能是检查队列是否可以抢锁
//这个非公平锁和公平锁逻辑不一致,在之后的文章再讲
if (!readerShouldBlock() &&
// 是否超出最大值
r < MAX_COUNT &&
// CAS尝试抢锁
compareAndSetState(c, c + SHARED_UNIT)) {
// 抢锁成功之后,如果是第一个进入则把 firstReader 参数赋予持锁线程
// 并对firstReaderHoldCount复制参数
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 重入 firstReaderHoldCount++
firstReaderHoldCount++;
} else {
// 不是第一个进入的会存入 cachedHoldCounter
// 如果cachedHoldCounter不存在 或者 cachedHoldCounter线程的Id与当前线程不同
// cachedHoldCounter 赋值为当前的线程
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
// 是原先的cachedHoldCounter 已经释放锁了
// 还没来得及改,于是重新把rh设置readHolds
else if (rh.count == 0)
readHolds.set(rh);
// 写锁++
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
2.1.2 readerShouldBlock方法
公平锁
//和ReentrantLock 逻辑一直
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
非公平锁
这段代码存在的是因为上一篇存在的读写不重入的原因导致的,如果读锁疯狂占用线程就有可能导致出现写饥饿的情况,写的线程会一直被锁住而得不到执行;这段代码的意义就是为了检查下一个线程是否是写锁,如果是写锁就将线程挂起,让写锁先执行来避免上述的情况。
//非公平锁方法
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
// head 不为空
return (h = head) != null &&
// head 的下一个不为空
(s = h.next) != null &&
// 且下一个锁不是被共享锁锁住
!s.isShared() &&
// 且节点的线程不存在
s.thread != null;
}
2.1.3 readerShouldBlock方法
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
//尝试直到有一个结果为止
for (;;) {
int c = getState();
//存在写锁挂起
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();
//在这里清除为0 cachedHoldCounter
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;
}
}
}
2.1.4 doAcquireShared
private void doAcquireShared(int arg) {
// 添加进入双向链表,详情见ReentrantLock的篇中有详细介绍
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取当前节点的前节点
final Node p = node.predecessor();
// 如果当前节与头结点一致尝试抢锁
//(因为双向链表的头结点是虚节点,头结点的后面一个节点就是排在第一个的)
if (p == head) {
int r = tryAcquireShared(arg);
//抢锁成功咯
if (r >= 0) {
// 简单来说就是去掉头结点,然后将当前节点设置为新的头结点
// 理解不了也可以理解成删除在链表中当前节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 负责轮询到前节点为 -1 的节点,详细讲解见ReentrantLock篇。
if (shouldParkAfterFailedAcquire(p, node) &&
//unsafe.park() 方法挂起线程,顺便检查一下线程是否被中断。
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
2.2 释放锁
以下是释放锁的方法,我们讲对内部方法逐个解析
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
2.2.1 tryReleaseShared 方法
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 如果释放的是第一个线程,则正常的 --
// 当释放最后一个的时候 firstReader 置为空
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
// 如果不是第一个则同加锁逻辑相同,赋值 cachedHoldCounter 并 --
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;
// CAS 的方式修改直到修改成功为止,如果为0则成功释放
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}