读锁重入次数怎么分别保存?读写锁的获取数量如何原子性修改?
其实之前在学习 Lock 的时候,学得比较粗糙,我也相信很多人都知道,像 ReentrantLock,ReadWriteLock 都是基于 AQS,CAS 实现的。
通过状态位(或者说标志位)state 来 CAS 抢锁,通过一个 AQS 链表队列,来实现线程的排队,LockSupport 来实现线程的阻塞与唤醒,通过模板方法设计模式,来对代码进行封装。
甚至,可以说基于这些思想,手写一个简化版 lock。
确实,如果能理解、学习到这些知识,已经足够去面试一些中小企业。只不过,相信很多人都有一颗积极向上的心,想要更好的就业平台,那么,这些知识还是比较 表层 的。
很多大厂的面试官只要一问,就知道,你是随便背了几篇博客,还是真正从 源码 角度去研究过。这些源码中的每一处细节,都是写作者的 思维的结晶,体现出 高度严谨、缜密 的编程思想。
学习忌浮躁
下面举几个 ReentrantReadWriteLock 的几道题目,你来简单检测一下自己的学习情况。
下面我会基于源码,对 ReentrantReadWriteLock 进行分析。不过在此之前,我需要你阅读过我的上一篇文章。
99%的人不知道AQS还有这种操作(源码分析ReentrantLock的实现和优秀设计)(写这么详细不信你还看不懂)
在上一篇文章中,我从源码,并且画图,十分详细地 讲解了很多的 AQS 的设计理念,方法实现过程。
因为 AQS 是一个抽象类,而 lock 不过是用一个 内部类 重写需要的方法完成自己的实现,所以很多地方都是互通的。
而从 ReentrantLock 开始,学习 AQS,是一个不错的入门,也是对基础的扎根。
上一篇文章我写得很详细,而这一篇文章则会有涉及很多重复的知识点,我都不再去写。
所以学完上一篇文章的知识,以此为基础,学习这里的知识便会很轻松。
你学习完上一篇 ReentrantLock 之后,再来看这一偏文章,就会发现许多相通之处。
此时,基于 互斥锁 和 共享锁,你便可以将内部知识连接起来,便融会贯通。
从此,再去学习其他有关 AQS 的代码时,便能心领神会,入眼即已了然。
要实现读锁共享,写锁互斥,那么就分别需要两个值来记录锁的重入次数
如果写锁的重入 >0,那么就应该阻塞住所有的其他抢锁线程
自己可以重入写锁,同时,也可以重入读锁
如果读锁的获取次数大于 0,那么此时一定没有写锁被获取,
要获取写锁的必须进入队列,在队列中阻塞
因为 CAS 只能对一个 int 操作,没有办法同时对 两个 int 操作
所以,如果用一个 state 记录读锁数,一个 state 记录写锁数就会出现问题(不加锁,没有办法对两个 int 进行原子操作)
在 JDK 源码中,我们发现这是用一个 int 来实现的(所以可以用 CAS 保证原子)
可是一个 int 如何表示两个值??
Doug Lee 很聪明,他把 32 位的 int,用前 16 位表示 共享锁,后 16 位表示 互斥锁。
获取互斥锁的 state
// 获取互斥锁的 state
static int exclusiveCount(int c) {
// 将整个int数 和 后16位都是1的二进制数做与运算
return c & EXCLUSIVE_MASK;
}
就像这样,做与运算的这个数,二进制表示的后 16 位全部是1,
00000000000000001111111111111111
那么运算结果只保留后 16 位的值,而前 16 位全部为 0,就相当于互斥锁的 state
对于修改,我们只需要对 CAS 的结果值 加上我们期待的值便可
compareAndSetState(c, c + acquires)
获取共享锁的 state
// 获取共享锁的 state
static int sharedCount(int c) {
// 将整个in整数无符号右移16位
return c >>> SHARED_SHIFT;
}
将整个整数无符号右移 16 位,那么最右边的数已经全部被移除界外没有了,而前 16 位则正好补在后 16 位,前面全部补 0,那么这样就能得到前 16 位的数字
设置共享锁的 state
compareAndSetState(c, c + SHARED_UNIT))
其中的 SHARED_UNIT = 1 << 16
用二进制表示就是:
0000000000000001 0000000000000000
后面 16位 都是0,就不会影响到共享锁的 state,每次加这个值,都相当于在前 16位 +1
首先调用了 sync 的 acquireShared() 方法
我们都知道 AQS 是一个 抽象类,是基于 互斥锁 和 共享锁 的 封装
之前学习的 ReentrantLock 的加锁方法,就是去调用 AQS 获取互斥锁的方法 acquire(),只不过对公平和非公平的 tryAcquire() 进行了不同的重写
而此处的 ReadLock 读锁属于共享锁,所以其中的 lock 调用了 AQS 的获取共享锁的方法 acquireShared(),所以很容易联想到,此处的 ReadLock 一定对 tryAcquireShared() 方法进行了重写
// 首先调用sync的模板方法
// sync集成了AQS,但不对该方法重写
// 所以本质上调用了AQS的 acquireShared 获取共享锁
public void lock() {
sync.acquireShared(1);
}
我们点进这个方法
首先尝试加锁,此时由于是第一次来加锁,之前还没有任何线程来过,所以一定会加锁成功
于是 tryAcquireShared 返回当前读锁的数目,也就是只有当前线程,返回 1。
// 获取共享锁
public final void acquireShared(int arg) {
// if 中尝试获取共享锁
// 此时一定获取到,返回 1
if (tryAcquireShared(arg) < 0)
// 这里就不执行
doAcquireShared(arg);
}
tryAcquireShared 方法是 AQS 没有实现的,此时由读锁进行实现
首先要获取 当前的线程 和 state 值
然后判断是否被写锁占有(我们知道写锁是互斥锁,会阻塞所有其他需要抢锁的线程),如果被写锁占有了,那么此时读锁一定会获取失败,直接返回 -1 表示失败
如果没有读锁获取,那么继续执行,获取读锁的占有数量
但是,在尝试 CAS 加锁之前,它先判断自己应不应该阻塞 !!!
有没有觉得很奇怪,这个时候没有写锁获取锁,按道理读锁应该去抢锁才对,但是,
它先判断要不要阻塞,
这是什么神仙逻辑????
你有没有想过
这其实跟我们之前看 ReentrantLock 的时候碰到的逻辑类似
在 ReentrantLock 中,公平锁 在 CAS 抢锁前,要先看一下队列里有没有人,不然就不抢锁
这里,读锁,首先 CAS 抢锁之前,我们应该看一下队列里排队的第一个是不是写锁,
如果下一个就是写锁了,那么就不该去抢锁
读锁,抢来抢去无所谓,因为可以共享,第一次抢完了,没抢到,根本不用等抢到锁的线程释放,就可以再去抢锁,把持有数 加上一
但是写锁不一样,它会阻塞,所以不能在写锁排队结束,要开始抢锁的时候,读锁去和写锁争抢
所以要在 CAS 抢锁前,先判断一次是否有读锁在最前边要出队了
此时,没有其他线程争抢,所以一定会获取,
然后设置 第一个读锁线程 为自己
但是,有个问题,它可以设置第一个持有读锁的线程为自己
那第二个线程获取读锁了怎么办???它拿什么记录自己???
有第二个的变量吗???
可是没有
那第二个抢读锁的线程该怎么记录??
后文解答
这时返回 1,最终让 lock() 方法返回,加锁就成功结束了
protected final int tryAcquireShared(int unused) {
// 开头与 ReentrantLock 的加锁方法如出一辙
// 先获取当前线程和state值
Thread current = Thread.currentThread();
int c = getState();
// 我们知道写锁是互斥锁,会阻塞其他任何要获取锁的线程
// 所以 如果写锁重入次数不为0,说明有人加了写锁
// 并且如果写锁不是自己(因为如果是自己,自己既可以重入写锁,也能加读锁)
// 那么肯定不能获取读锁
// (此时由于第一次来,所以肯定没有锁在,所以不进 if)
if (exclusiveCount(c) != 0 &&
// 如果写锁不是自己
getExclusiveOwnerThread() != current)
return -1;
// 获取读锁的占有数量
// (读锁是很多线程都能获得的)
int r = sharedCount(c);
// 先判断读锁是不是应该被阻塞
/**
* 这里你有没有觉得奇怪,为什么互斥锁没人持有
* 还要先判断要不要阻塞???
* 没人持有锁不应该去抢锁吗?
*/
if (!readerShouldBlock() &&
// 再判断数量有没有超过上限(正常情况下不会)
r < MAX_COUNT &&
// 这时候 CAS 去获取读锁
// 抢锁成功就进入了if中的方法
compareAndSetState(c, c + SHARED_UNIT)) {
// 如果本来没有人持有读锁,那么自己设置成第一个读锁
/**
* 但是你有没有想过,第二个线程怎么办
* 第一个占了 第一个读锁的变量
* 但是有第二个读锁变量吗??
* 没有
* 那第二个线程如何保存重入次数??
* 后文解答
*/
if (r == 0) {
firstReader = current;
// 重入次数设置 1
firstReaderHoldCount = 1;
// 如果是自己来重入的,那就把重入次数加 1
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
// 这里不进 else 方法,我们先不看
// ... 省略无关代码
}
return 1;
}
// 没有进if
// 可能是判断需要需要阻塞
// 可能是 CAS 抢锁失败
// (正常情况不会数量达到上限的,一个 JVM 上抢几万个读锁也太夸张了)
// 于是进入 fullTryAcquireShared 方法(传入当前线程做参数)
return fullTryAcquireShared(current);
}
第一次加锁很简单
加锁的过程和之前相同,都是:
关键是抢锁结束后的操作不同
之前第一个线程读锁抢锁后,设置自己为第一个读锁线程,然后重入设置为 1
这时不是同一个线程了,但是 !!
没有第二个线程的变量给它直接设置为自己,也没有直接的 重入次数给它记录。
所以用到了一个计数器的类 HoldCounter,专门来记录读锁的重入次数
(这样用一个每个线程专有的计数器,保证了每个线程的读锁重入次数都能够记录)
// 进入尝试加读锁的方法
protected final int tryAcquireShared(int unused) {
// ...省略相同代码(忘记的回到前面复习)
} else {
// 这一次由于不是第一个线程获取读锁了,所以进 else 方法
// 首先获取一个 统计重入数量的 缓存
// 因为第一次获取,所以一定是 空(null)
/**
* 这个东西是用来统计共享锁的重入次数的
*/
HoldCounter rh = cachedHoldCounter;
/**
* 如果为空 或者 它的线程id不是当前线程id
* 说明没有直接拿到当前线程的 重入计数器
* 那就要去 readHolds 取出它的计数器
*/
if (rh == null || // 空
rh.tid != LockSupport.getThreadId(current))//不是当前线程的
// 从 readHolds 取出当前线程的计数器
cachedHoldCounter = rh = readHolds.get();
// 如果计数器为 0,说明还没用过
// 不敢保证 readHolds 中一定含有这个计数器,所以要把它放进去
else if (rh.count == 0)
readHolds.set(rh);
// 计数器的值+1
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
一共有两个变量,count 和 tid
static final class HoldCounter {
int count; // initially 0
// 创建计数器初始化时 LockSupport 获取线程 id
// 并且 final 不可改变保证了安全
final long tid = LockSupport.getThreadId(Thread.currentThread());
}
首先我们可以看到 readHolds 是一个 ThreadLocalHoldCounter 对象
private transient ThreadLocalHoldCounter readHolds;
点进去,发现实际上,不过是一个 ThreadLocal
因此每个 不同的线程 都可以从 readHolds 里取出属于自己的 HoldCounter 计数器
(ThreadLocal 不知道的先去补补课。。。)
// 本质上是一个 ThreadLocal
static final class ThreadLocalHoldCounter
// 泛型用 HoldCounter计数器 替换
extends ThreadLocal<HoldCounter> {
// 重写了初始化方法,这样使得第一次获取的时候不为null
// 而是一个 属于自己的 新的HoldCounter计数器
public HoldCounter initialValue() {
return new HoldCounter();
}
}
其实我觉得这个不说你们应该也能自己想出来
(但是我害怕,你们万一犯懒了,没有去想,那会不会就遗漏了点东西)
(我真是用心良苦 T_T)
这里很明显是以空间换时间的思想
首先,每一次操作的 HoldCounter计数器,都会缓存在这个变量中
这样,如果下一次使用同样的计数器,就不用从 readHolds 中去取了
所以这样对同一个读锁线程重入可以提高效率(因为这个 计数器被缓存了)
但是由于只缓存一个,所以一直切换线程重入读锁,就起不到提高效率的作用
可以发现,从第二个来获取读锁的线程开始,都是用 TheadLocal 来进行记录的
但是,为什么第一个要单独保存???
其实我觉得,看完上一个 cachedHoldCounter 的作用,你应该已经能明白
对于只有一个线程获取读锁的情况下,就不用用到 readHolds。
这样每次都能直接拿到变量,对重入次数进行操作
而不用去 readHolds 中把计数器找出来,就能提高效率
而对于多个线程的情况下,其它线程就不能提高效率了
首先和之前一样
但是抢锁成功之后就不同了
当时说了 3 种情况
这些情况会使得代码进入最后一行的方法(fullTryAcquireShared),并传入当前线程作为参数
protected final int tryAcquireShared(int unused) {
// ...省略无关代码
if (!readerShouldBlock() &&
r < MAX_COUNT &&
// 竞争条件总有线程会 CAS 失败
compareAndSetState(c, c + SHARED_UNIT)) {
// ...省略无关代码
}
// 所以不会进 if
// 会进入该方法(传入当前线程作为参数)
return fullTryAcquireShared(current);
}
当时判断有写锁线程正准备出 AQS 队列抢写锁,方法叫 readerShouldBlock(读锁应该阻塞)
但是它并不是立刻就真的阻塞了
而是运行到这个方法里面之后,又进行了一次重复的判断,如果这次的 readerShouldBlock 还是返回 true,那么才真的让方法返回 -1,结束该方法
(这个方法和上个方法有很多地方类似、冗余,很容易理解)
不过想一想,这样有什么好处??
为什么不直接阻塞,而是要多给它一次机会去再判断一次??
如果我的上一篇 ReentrantLock 的 AQS 讲解你们看了,我觉得你们应该能反应过来
这就是多自旋一次,避免直接阻塞
还有:
这一次也会重新判断是不是,读锁的获取数量是否爆满,这一次如果还是爆满,那就抛出异常了
当然最常见的还是 CAS:
第一次 CAS 失败后,会进入到这个方法继续给它机会去不断地 CAS(只要没有写锁来阻塞它)
我们看代码:
// 读锁加锁方法,会不断循环
// 直到成功,或发现写锁,需要阻塞
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
// 熟悉的死循环
// (拿不到锁誓不罢休)
for (;;) {
// 获取 state
int c = getState();
// 判断是否有其他线程占有写锁
// 如果有就 返回 -1 加锁失败
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {
/**
* 熟悉的 应该阻塞方法
* 此时需要阻塞,所以必须加锁失败,返回 -1
* (除非是来重入的,也就是之前已经加过锁了,
* 那么此次加锁,不必阻塞)
*/
// 如果不是第一个加读锁线程
// (如果是的话,肯定是来重入的,就不用判断了)
if (firstReader == current) {
} else {
// 否则,就去找它的计数器,如果不为0,那么也是来重入的
if (rh == null) {
rh = cachedHoldCounter; // 缓存中找
if (rh == null ||
rh.tid != LockSupport.getThreadId(current)) {
// 缓存没就去 readHolds 中拿
rh = readHolds.get();
// 如果为 0,那么说明不是来重入的,此时要 返回-1
// 但是在返回 -1 之前,要把 readHolds 中的记录先清除(避免内存泄漏)
// (ThreadLocal不手动清除会内存泄漏)
if (rh.count == 0)
readHolds.remove();
}
}
// 前面已经清楚了readHolds的记录,这时可以返回了
if (rh.count == 0)
return -1;
}
}
// 如果读锁获取数量爆满了,就抛异常(一般不会)
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// CAS 抢锁
if (compareAndSetState(c, c + SHARED_UNIT)) {
// (感觉这个if永远不会执行到,因为这是在CAS成功的条件下执行的
// 所以至少会有 sharedCount 至少也要是 1)
// 也许是 Doug Lee 只是为了保证代码不会出错才写的吧
// 如果你们发现有这个情况的话可以给我留言
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} // 后面的代码与之前的无太大差别
else if (firstReader == current) { // 重入
firstReaderHoldCount++;
} else { // 将自己的计数器 +1
if (rh == null)
rh = cachedHoldCounter;
if (rh == null ||
rh.tid != LockSupport.getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1; // 加锁成功,返回 1
}
}
}
我们在上文分析过,
如果没有写锁来阻塞的话,那么在 tryAcquireShared 方法中,读锁一定会在某一刻抢到锁(默认不要管爆满情况)
所以 tryAcquireShared 就会返回获取读锁的数量,也就不会 <0
假设,这时,有写锁来了,那么读锁的 tryAcquireShared 就会失败,返回 -1
那么这时就会运行到下面的方法 doAcquireShared
public final void acquireShared(int arg) {
// 尝试加锁失败,说明碰到了写锁
if (tryAcquireShared(arg) < 0)
// 那么才会进下面的方法
doAcquireShared(arg);
}
看到这里我们会发现,和之前我们阅读 ReentrantLock 时的方法非常类似
首先也是调用 addWaiter 入队,但是,
注意,这里的 addWaiter 传入了一个新的参数,叫 Node.SHARED
然后就会进入一个死循环,开始熟悉的套路
这里的套路和之前一模一样
又是两次自旋,
第一次,会将前一个等待的线程的结点的 ws 设置为 -1
然后第二次,看到前一个 ws 是 -1,于是开始 阻塞
private void doAcquireShared(int arg) {
// 熟悉的 addWaiter 方法
// 但是注意,这里的传的参数就不一样了
// 用带 Node.SHARED 参数的构造方法,创建了结点
// (表示这是要获取共享锁的线程的结点)
final Node node = addWaiter(Node.SHARED);
// 下面的方法和我们学习过的一致
boolean interrupted = false; // 记录是否被打断过
try {
// 熟悉的死循环
for (;;) {
// 获取前置结点
final Node p = node.predecessor();
// 如果前面的结点就是头结点
if (p == head) {
// 这时就再次尝试加锁
int r = tryAcquireShared(arg);
// 如果加锁成功,就将自己设置为头结点
// 不过要注意,此时的设置头结点和ReentrantLock的互斥锁有所不同
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
return;
}
}
// 如果应该park
if (shouldParkAfterFailedAcquire(p, node))
// 然后开始park
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
} finally {
if (interrupted)
selfInterrupt();
}
}
之前我们学习 ReentrantLock 的时候,头结点的设置仅仅是:
把前一个头结点剔除,自己充当头结点,并且 thread 属性设置为 null(当前持有锁线程不在队列中)
而此处的 读锁 则不同,在调用 setHead 方法之后,
要去唤醒后面的所有读锁
之前学习 ReentrantLock 的时候,我们知道,互斥锁只会去唤醒后面那一个正在等待的线程
而读锁是共享的,不能只有自己一个线程被唤醒,
所以要去后面叫醒所有的读锁,才能让读锁一起共享。
private void setHeadAndPropagate(Node node, int propagate) {
// 设置头结点
Node h = head; // Record old head for check below
setHead(node);
// 我们是加锁成功后来到的这个方法,所以 propagate 的值 >0
// 这时我们进 if 方法
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
// 获取下一个结点
Node s = node.next;
// 如果 为空 或者是 共享锁
// 就去释放共享锁
if (s == null || s.isShared())
doReleaseShared();
}
}
唤醒共享锁的方法,是读锁特有的,
因为传统的互斥锁,只会去叫醒身后的那一个线程,
而读锁,是多线程共享的,因此要去唤醒身后的读锁,才能大家共享
private void doReleaseShared() {
// 死循环,用来唤醒下一个线程
// 其中如果CAS失败,就会continue重新一轮循环来操作
for (;;) {
// 获取头结点
Node h = head;
// 头不为空,并且不是尾(说明队列中有其他线程在排队)
// (否则说明没有其他线程在等)
if (h != null && h != tail) {
// 获取 ws
int ws = h.waitStatus;
// 如果是 -1 (SIGNAL = -1)
// (我们之前学习过ReentrantLock,已经知道 -1 表示后面有线程在等待)
if (ws == Node.SIGNAL) {
// 这时 CAS 把 -1 改成 0
if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
continue; // 否则,说明被其他线程改了,那就去下一次循环
// 改成功就唤醒下一个线程
unparkSuccessor(h);
}
// 如果 ws 为 0 (说明下一个线程开始排队了,但还没开始阻塞)
else if (ws == 0 &&
// 那就用 CAS 改成 -3
// (因为改成功了就保证它并没有开始阻塞)
!h.compareAndSetWaitStatus(0, Node.PROPAGATE))
// 如果 CAS 失败,就进行下一次循环
// 因为可能 ws 已经从0,变成了-1,这时就需要多一个循环去将其唤醒
continue;
}
// 如果 h == head,就可以结束循环
// 如果head被修改,说明队列状态改变,则重新进行一次循环
if (h == head)
break;
}
}
不管怎么样,互斥锁的加锁都会调用 AQS 的模板方法 acquire
这里我在 ReentrantLock 里面详细分析过了
我们需要关注的,则是 它们的不同点,子类重写的 tryAcquire 方法
// final 模板方法
public final void acquire(int arg) {
// tryAcquire 由子类实现,是不同之处
if (!tryAcquire(arg) &&
// 后面代码都是相同的
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
AQS 你把之前基础的方法和队列关系学习理解之后,其实你再来看这些方法,都是类似的,
像这些方法,都是一些基本套路的简单修改
ReadWriteLock 的 写锁 和 ReentrantLock 一样,都是互斥锁
所以它们的方法都是共用一个 模板方法
而不同点,只是在于它们重写的 tryAcquire 而已
protected final boolean tryAcquire(int acquires) {
// 先获取当 前线程和 state值
Thread current = Thread.currentThread();
int c = getState();
// 获取出 互斥锁的重入次数(state的后16位)
int w = exclusiveCount(c);
// c不为0,说明有线程持有锁(不管是读锁还是写锁)
if (c != 0) {
// 如果w(互斥锁的重入次数)为0,那么目前只有读锁被线程持有
// 或者 持有写锁的线程不是当前线程
if (w == 0 || current != getExclusiveOwnerThread())
return false; // 这样就无法获取写锁
// 如果互斥锁的重入次数,加上这次,就超过上线(正常没人重入那么多次。。)
if (w + exclusiveCount(acquires) > MAX_COUNT)
// 就抛异常(不过一般不会发生)
throw new Error("Maximum lock count exceeded");
// 前面的 if 都没有进
// 那么说明目前只有自己持有锁,可以重入
setState(c + acquires);
return true;
}
// c为0,那么说明目前没有任何线程持有锁
// 还是先判断 写锁是否应该阻塞
// (不过这个方法,写锁的重写是直接 返回 true)
// 也就是说,这里的写锁是非公平的(不像ReentrantLock,可以公平)
if (writerShouldBlock() ||
// 因为非公平,所以不管怎样都允许抢锁
// 因此重写的方法一定返回 true,所以会进行CAS加锁
!compareAndSetState(c, c + acquires))
return false; // 加锁不成功返回 false
// 老套路,加锁成功设置互斥锁的持有者为当前线程
setExclusiveOwnerThread(current);
return true;
}
我们可以看到,写锁重写的 writerShouldBlock 方法直接返回 false
可见 ReentrantReadWriteLock 的写锁是非公平的
final boolean writerShouldBlock() {
return false; // writers can always barge
}
同样的,类似 ReentrantLock,解锁方法调用 release 方法
写锁只是对 tryRelease 方法进行了重写
// 互斥锁模板方法
public final boolean release(int arg) {
// 我们只需要关注 重写的 tryRelease 方法
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
同样的,类似 ReentrantLock
都是对 互斥锁的重入次数 -1
只有当重入的次数变为 0,这时说明锁已经完全释放,
于是设置持有锁的线程为 null,然后返回 true;
否则返回false。
protected final boolean tryRelease(int releases) {
// 如果持有锁的不是当前线程,抛异常(很好理解,不解释)
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 这里直接获取 state 作为写锁的重入次数
// 虽然写锁只有 后16位 !
// 不过,由于写锁占有者锁,所以读锁的 前16位 一定全为0
// 所以,此时的 state 的值就等于 写锁的重入次数
// (就没必要再 与运算 了)
int nextc = getState() - releases;
// 减了 1 之后,看看互斥锁的重入数 是不是 0
boolean free = exclusiveCount(nextc) == 0;
// 0的话表示,锁已经完全释放
if (free)
// 然后就可以设置互斥锁的 持有线程为 空
setExclusiveOwnerThread(null);
// 将 state 写回内存
setState(nextc);
// 返回是否完全解锁
return free;
}
其实,一开始我是以为这篇文章我会写很久很久,因为之前写 ReentrantLock 的时候,花了几天时间才写完。
而且我写 ReadWriteLock 的时候,之前也没有直接阅读过源码。
结果只用了八九个小时,我就将其写完了。
但是,写着写着才发现,其实很多地方都是共通的,很多知识点和我在 ReentrantLock 里面写到的知识点都相同。
毕竟它们都是基于 AQS 实现的。掌握了 AQS 的核心,这些实现便很容可以领会。
所以我很推荐,大家从我的那篇 ReentrantLock 的文章来开始学习。
我从底层的源码,几乎将所有涉及的知识点,和其中的设计思想都给讲到了。
所以这一篇文章,我也没有花精力去画图,也没有去讲解知识点重复的代码段,因为通过上一篇 ReentrantLock 的学习,如果真的学习明白了,那你们就要已经具备了直接能看懂这些代码的能力。
而且我认为,你们也完全可以不用看博客,直接阅读源码进行学习。
不过,实际上,很少有用心学习的。很多人对源码的惧怕,很多人对阅读和分析的懒惰,致使 AQS 真正理解的人不多。
其实这也无妨,毕竟对技术的学习,本来就会有精通的人,也本来就会有只是需要使用的人。
所以,我的文章博客,也仅需要供给给那些真正需要,从底层掌握透彻的人。