StampLock的源码详细剖析
StampLock的源码详细剖析
欢迎来到我的博客:TWind的博客
我的CSDN::Thanwind-CSDN博客
我的掘金:Thanwinde 的个人主页
0.前言
StampLock是ReentrantReadWriteLock的一个变种,也可以说是一种升级
对于ReentrantReadWriteLock来说,虽然说读锁是共享锁,但还得竞争CAS读计数,而StampLock对于读完全不加锁,仅采用邮戳(stamp)读完之后看自己读的版本对不对,这种“乐观读”的方式极大的提升了读 >> 写的效率
那么,代价是什么呢?
StampLock不支持可重入,不支持条件队列,以及读的中断响应,而且没有像是ReentrantReadWriteLock,ReentrantLock的API封装:你需要自己去处理stamp的验证,锁的升级等等,很繁琐,具体的实现也很繁琐
显然用的也不是AQS的同步队列,更为复杂
所以你可以理解为StampLock是对于读高并发这种极端情况而研究出的一种极端应对方案
建议先阅读完《ReentrantLock的详细源码剖析》,《ReentrantReadWriteLock的源码详细剖析》再行阅读
对于acquireWrite,acquireRead,cancelWaiter并不会源码一行行解析,因为其实在是太过冗长复杂且可读性极差,有兴趣的读者可以自行参阅
1.StampLock的重要参数与节点结构
参数
我们进入StampLock,开头会看到一大堆五彩斑斓的参数:
private static final int NCPU = Runtime.getRuntime().availableProcessors();private static final int SPINS = (NCPU > 1) ? 1 << 6 : 0;private static final int HEAD_SPINS = (NCPU > 1) ? 1 << 10 : 0;private static final int MAX_HEAD_SPINS = (NCPU > 1) ? 1 << 16 : 0;private static final int OVERFLOW_YIELD_RATE = 7; private static final int LG_READERS = 7;private static final long RUNIT = 1L;
private static final long WBIT = 1L << LG_READERS;
private static final long RBITS = WBIT - 1L;
private static final long RFULL = RBITS - 1L;
private static final long ABITS = RBITS | WBIT;
private static final long SBITS = ~RBITS; private static final long ORIGIN = WBIT << 1;/*...........*/private transient volatile long state;private transient int readerOverflow;
为什么会有如此之多的位运算?
原因在于StampLock用了一个长整型
private transient volatile long state;
来同时存储:版本号(9-64位),写锁位(第8位),读锁数(后7位)
有点类似于ReentrantReadWriteLock的用一个state来存储读锁和写锁,但是StampLock存得更多也更复杂
让我们从头到尾解释一下这些参数:
-
NCPU:是你的CPU可用的核心数,会以此来设定你的自旋次数(其实也就来判断 单核 or 多核)
-
SPINS:进入同步队列之前的自旋数,单核1,多核64
-
HEAD_SPINS:线程已排到队首时再次忙等的次数,单核1,多核1024
-
MAX_HEAD_SPINS:如果多次在队首失败,将自旋次数指数级抬升但不超过此值,单核1,多核65536
-
OVERFLOW_YIELD_RATE:处理读计数溢出时,每当自旋循环次数 &
OVERFLOW_YIELD_RATE
==0 就Thread.yield()
,固定为7state位运算参数:
-
LG_READERS:为7,低 7 bit 用来保存读锁计数(0-127)。
-
RUNIT: 为1L,“一个读者”的增量。获取/释放读锁就是对
state
加/减此值。 -
WBIT:为
1L << LG_READERS
也就是左移7位,即128,上面说到读锁计数只有7位,第八位是写标记,这里对应的就是写标记 -
RBITS:是
WBIT-1
,即127,这样用state & RBITS
就能知道当前读者数 -
RFULL:为RBITS-1,代表读计数的最大正常值,即126,当达到 127 时进入“溢出”路径。
-
ABITS:
RBITS | WBIT
,用于表示锁计数和写标记的,其为0b11111111 -
SBITS:
~RBITS
,结果除了后七位全是1,即0b1111111,用来获取版本号+ 写标记 -
ORIGIN:
WBIT << 1
代表state的初值,初生版本号 = 1,写锁位为0,读锁数为0
至于为什么7位,明明可以存储2^7 - 1= 127个数,为什么RFULL是126?
原因在于,StampLock把127作为一个符号位,当发现其后七位为127,视为“溢出状态”,就会启用另一套逻辑来对其进行处理
也就是会用readerOverflow来存储
节点
数据结构方面,StampLock并没有像其他JUC锁一样采用AQS同步队列
其采用了自己的一个CLH队列,但大体结构相同,让我们看看其节点:
private static final int WAITING = -1;
private static final int CANCELLED = 1;private static final int RMODE = 0;
private static final int WMODE = 1;static final class WNode {volatile WNode prev;volatile WNode next;volatile WNode cowait; volatile Thread thread; volatile int status; final int mode; WNode(int m, WNode p) { mode = m; prev = p; }
}private transient volatile WNode whead;private transient volatile WNode wtail;
WAITING,CANCELLED表示节点状态,-1为等待中,1为已取消节点
RMODE,WMODE代表这个节点是读锁,写锁
接下来是节点:
prev,next,cowait代表一个节点的前驱,后继,同行节点:StampLock的队列中一个位置可以同时安插多个节点一并处理
thread,status代表这个节点的线程以及它的邮戳
whead,wtail代表其头结点以及尾节点
在大多数方法中,这个队列并不会被用到,因为其是用来处理必须堵塞的状态的
2.老方法“适配器”
视图(view)
虽然StampLock有些“超凡脱俗”,但其仍然封装了一些和其他juc相同的接口,称为视图:
final class ReadLockView implements Lock {public void lock() { readLock(); }public void lockInterruptibly() throws InterruptedException {readLockInterruptibly();}public boolean tryLock() { return tryReadLock() != 0L; }public boolean tryLock(long time, TimeUnit unit)throws InterruptedException {return tryReadLock(time, unit) != 0L;}public void unlock() { unstampedUnlockRead(); }public Condition newCondition() {throw new UnsupportedOperationException();}
}final class WriteLockView implements Lock {public void lock() { writeLock(); }public void lockInterruptibly() throws InterruptedException {writeLockInterruptibly();}public boolean tryLock() { return tryWriteLock() != 0L; }public boolean tryLock(long time, TimeUnit unit)throws InterruptedException {return tryWriteLock(time, unit) != 0L;}public void unlock() { unstampedUnlockWrite(); }public Condition newCondition() {throw new UnsupportedOperationException();}
}
你可以看到,里面的方法都非常熟悉:lock,lockInterruptibly,tryLock,unlock
这就可以在原来使用了ReentrantReadWriteLock的代码一键更改成StampLock
但是,如果采用了视图来适配老接口,就无法使用StampLock特有的 “乐观读”,将会全部由StampLock的队列处理
但尽管如此,也有很大的性能提升,同样的,代价是不支持重入,条件队列,也用不到StampLock的锁升级/降级
[!WARNING]
对于JUC,带有try的前缀的方法只会尝试一次,不会自旋
就算会自旋也通常会自旋一两次,不会一直自旋
3.读方面
tryOptimisticRead()
public long tryOptimisticRead() {long s;return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}
这就是大名鼎鼎的“乐观读”,十分的简单粗暴
这里的 return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L; 便是精华:
(s = state) & WBIT,s会被赋值为当前的state,随后将其 & WBIT,我们翻阅上面,可以知道WBIT是1L左移八位,这里也就是判断写标记是不是0,是的话就返回s & SBITS,结果就是版本号+写标记位(肯定是0)
比如 state是 0…001 0 1234567,就会截成0…001 0 0000000返回
这也是乐观读的特别之处:返回的stamp只有版本号,其它地方是0
拿到stamp后,程序就会开始读,读完之后再检测一下版本号变没有,没变就结束,变了说明中间有人修改了,就重新读或者升级成读锁
下面是一个实例程序:
public void optimisticRead() {//获取邮戳long stamp = lock.tryOptimisticRead();// 开始读copyVaraibale2ThreadMemory();// 读完看看版本变没有if (!lock.validate(stamp)) {// 没通过,重复或者升级成读锁stamp = lock.readLock();try {// 重新读copyVaraibale2ThreadMemory();} finally {// 释放读锁lock.unlockRead(stamp);}}// 读完了,操作数据useThreadMemoryVarables();
}
可以看到,StampLock并没有提供一个比较”全能“的API,具体的逻辑是要自己实现的,这也是使用乐观锁的代价之一
让我们看看检验的方法:
validate()
public boolean validate(long stamp) {U.loadFence(); //加入读屏障return (stamp & SBITS) == (state & SBITS); //看看邮戳和state版本号一不一致
}
那么,U.loadFence()是什么?
我们在下面可以看到:
U = sun.misc.Unsafe.getUnsafe();
loadFence也就是加入一个读屏障,读屏障之前发出的 所有读指令 必须在栅栏之前完成
常规来说,我们的读取期望是: loadData -> validate -> dealWithData
但,由于JIT的代码重排,可能会变成: valiadate ->loadData ->dealWithData
要是中间有一个写操作插入了,就会变成 valiadate -> writeData ->loadData ->dealWithData
我们就会读到错误的数据
为什么会出现这种情况?可以参阅我的《关于JVM和OS中的指令重排以及JIT优化》
这样的话就能保证读到的数据一定发生在版本校验之前
tryConvertToReadLock()
如果乐观读出错了,就有可能会被升级为读锁,具体会怎么操作取决于调用者,以下是升级的代码:
public long tryConvertToReadLock(long stamp) {long a = stamp & ABITS, m, s, next; WNode h;//这里的a是写标记+读锁数while (((s = state) & SBITS) == (stamp & SBITS)) {//这里把s赋成state,&SBITS得到版本号+写标记,这里校验当前传入的stamp是否和当前版本一致if ((m = s & ABITS) == 0L) {//m为后八位,说明没有读锁和写锁if (a != 0L) //state后八位为0,但你的stamp不为0,说明你本来就有锁 break;else if (m < RFULL) { //判断溢出没有,溢出了就走溢出的通道if (U.compareAndSwapLong(this, STATE, s, next = s + RUNIT))return next; //CAS修改读锁计数,然后返回新的版本号}else if ((next = tryIncReaderOverflow(s)) != 0L) return next; //走溢出路线}else if (m == WBIT) { //已经存在写锁if (a != m) //a是写标记+读锁数,如果a == m,一般情况下你是没法拿到写标记为1的stamp的,那么,只能你就是写锁持有者break;state = next = s + (WBIT + RUNIT); //更新状态if ((h = whead) != null && h.status != 0) //如果存在合法的后继节点,就唤醒release(h);return next; //返回新的版本号}else if (a != 0L && a < WBIT) //还没有写锁,但有读锁,那直接返回就行,可以读return stamp;elsebreak; }return 0L;
}
这只是个可选方案,我们可以把自己之前拿到的stamp升级成读锁,也可以直接新创建一个读锁
执行完tryConvertToReadLock并返回一个stamp就视为我们已经拿到了一个读锁了,接下来的使用,释放都得我们自己实现
溢出处理如下:
tryIncReaderOverflow()
private long tryIncReaderOverflow(long s) {if ((s & ABITS) == RFULL) { //看看是不是是126,如果正在被修改就是127,就会失败或小概率重试if (U.compareAndSwapLong(this, STATE, s, s | RBITS)) { //先把后七位全部置成1,提醒别的线程“我正在处理溢出”++readerOverflow; //溢出计数++state = s; //恢复计数return s; }}else if ((LockSupport.nextSecondarySeed() &OVERFLOW_YIELD_RATE) == 0) //见下文Thread.yield();return 0L;
}
对于上方,很好理解,但让我们来看看下方:
能进入下方的if说明其他线程正在修改溢出值,那么会调用LockSupport.nextSecondarySeed(),这会产生一个伪随机数,让其 & 0x3F,只有1/64的概率成立,
也就是说,会有1/64的线程会yield让出cpu,防止高并发下的无用自旋
tryDecReaderOverflow
private long tryDecReaderOverflow(long s) {// assert (s & ABITS) >= RFULL;if ((s & ABITS) == RFULL) {if (U.compareAndSwapLong(this, STATE, s, s | RBITS)) {int r; long next;if ((r = readerOverflow) > 0) {readerOverflow = r - 1;next = s;}elsenext = s - RUNIT;state = next;return next;}}else if ((LockSupport.nextSecondarySeed() &OVERFLOW_YIELD_RATE) == 0)Thread.yield();return 0L;
}
都说的到了加溢出,那顺便连着减溢出也说一下:
先判断是不是126,不是的话说明有其他线程在修改,那就直接失败或者小概率自旋
如果进入了,就CAS修改状态,然后看看溢出部分是不是0,不是就用溢出部分正常减,是的话就直接用state减就行
readLock()
这就是常规的加读锁了:先会直接尝试CAS修改,可以的话就直接返回当前stamp,不然就通过acquireRead来加入队列
public long readLock() {long s = state, next; return ((whead == wtail && (s & ABITS) < RFULL &&U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?next : acquireRead(false, 0L));
}
需要注意的是,whead == wtail代表只有一个节点,这个节点往往是一开始的占位节点,也就是说,只有当队列为空,未溢出,CAS成功才能直接返回,不然都是会进入队列
[!IMPORTANT]
acquireRead方法在后面再介绍,因为实在过于复杂
unlockRead()
public void unlockRead(long stamp) {long s, m; WNode h;for (;;) {if (((s = state) & SBITS) != (stamp & SBITS) || //判断版本号和写标记是不是和当前相同,不相同意味着期间必然出现过写锁 //那么你早就不在读锁集合里,就抛出异常(stamp & ABITS) == 0L || (m = s & ABITS) == 0L || m == WBIT) //如果stamp/当前没有读锁或传入的是写锁的stampthrow new IllegalMonitorStateException(); if (m < RFULL) { //溢出了就走溢出的通道if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) {if (m == RUNIT && (h = whead) != null && h.status != 0)release(h); //尝试CAS修改,然后看看有没有后继的节点要唤醒break;}}else if (tryDecReaderOverflow(s) != 0L)break;}
}
非常的显而易见:先判断stamp是否合法,合法就一直CAS尝试修改state
注意,对于在队列中的读锁来说,版本号一定不能变,会导致版本号变化的唯一可能就是有写锁,那你照理来说不可能拿到读锁,就直接抛异常
判断stamp时,其中的m == WBIT,说明这时是没有读锁,而且有写锁,那在这种情况下你还有stamp的话,要么你自己有写锁,或者你误用了,无论如何都不合法
release()
private void release(WNode h) {if (h != null) { //h不为空节点的话WNode q; Thread w;U.compareAndSwapInt(h, WSTATUS, WAITING, 0); //就把h的状态改成0,标记为已处理if ((q = h.next) == null || q.status == CANCELLED) { //如果后继节点为空或者已取消for (WNode t = wtail; t != null && t != h; t = t.prev)if (t.status <= 0) //就会从后向前遍历找到最解决该节点的可用节点作为新的后继节点q = t;}if (q != null && (w = q.thread) != null) //如果后继节点不为空且有线程就将其唤醒U.unpark(w);}
}
这里涉及到了一些队列的内容,但不复杂,可以讲一下
这里的模式非常像AQS的同步队列,包括节点的状态,节点后继的判断
并不是很复杂,但只要了解就行,更深的就需要去看acquire系的代码,非常复杂且可读性极差,看个人喜好吧
tryUnlockRead()
会尝试释放一次读锁:不像unlockRead会一直自旋尝试
public boolean tryUnlockRead() {long s, m; WNode h;while ((m = (s = state) & ABITS) != 0L && m < WBIT) { //看看当前的state是不是有读锁,而且没有写锁,不满足直接失败if (m < RFULL) { //溢出了的话走溢出通道if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) {if (m == RUNIT && (h = whead) != null && h.status != 0)release(h); //有合法的后继就唤醒return true;}}else if (tryDecReaderOverflow(s) != 0L)return true;}return false;
}
这个方法并不像其他的锁那样,try和不try是内联的
更像是直接代码复制了一份过来,为什么?
我们之前提到了,StampLock是需要用户自己去写框架的,这些功能也就没有连在一起了
而且,tryUnlockRead并没有传进来stamp,默认你已经检验过了,像是unlockRead那样
unstampedUnlockRead()
final void unstampedUnlockRead() {for (;;) {long s, m; WNode h;if ((m = (s = state) & ABITS) == 0L || m >= WBIT) //检测如果没有读锁,或者存在写锁就直接抛异常throw new IllegalMonitorStateException();else if (m < RFULL) {if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) {if (m == RUNIT && (h = whead) != null && h.status != 0)release(h); //否则就正常走那套逻辑break;}}else if (tryDecReaderOverflow(s) != 0L)break;}}
这是一个不用stamp就释放读锁的方法,为什么要有这样一个方法呢?
在前面的视图中,我们并没有介绍其中的方法,现在可以告诉你:tryUnlockRead
为什么?你既然要适配老方法,老方法又没有stamp,那你必须提供一个方法来实现
本质上就是for里面套了一个tryUnlockRead
asReadLock()
public Lock asReadLock() {ReadLockView v;return ((v = readLockView) != null ? v :(readLockView = new ReadLockView()));
}
懒加载一个readLockView,在是null的情况下就new一个新的过去,不然就直接返回
这是配合视图使用的,很简单
4.写方面
tryConvertToWriteLock()
public long tryConvertToWriteLock(long stamp) {long a = stamp & ABITS, m, s, next; //这里a是后八位while (((s = state) & SBITS) == (stamp & SBITS)) { //看看版本号一不一样,不一样直接返回if ((m = s & ABITS) == 0L) { //后八位为0,没有写锁也没有写锁if (a != 0L) //如果写标记不为0,说明已经有写锁,直接返回break;if (U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) //cas修改写锁return next;}else if (m == WBIT) { //m是state的后八位,代表有写锁,没有读锁if (a != m) //a是stamp的后八位,两者不相等直接退出break; return stamp; //两者相等,说明写锁就是自己,那直接返回stamp就行}else if (m == RUNIT && a != 0L) { //这里说明有一个读锁,且a != 0,说明你是锁,没写锁,说明读锁就是自己if (U.compareAndSwapLong(this, STATE, s, //直接CAS修改,去掉读锁加上写锁next = s - RUNIT + WBIT))return next;}elsebreak;}return 0L;
}
这个方法和tryConvertToReadLock基本一样
检测你给的stamp是是乐观锁的,还是读锁,写锁的,然后对应的执行对应的操作
这里升级失败之后返回false,不会对队列操作
writeLock()
public long writeLock() {long s, next; // bypass acquireWrite in fully unlocked case onlyreturn ((((s = state) & ABITS) == 0L &&U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?next : acquireWrite(false, 0L));
}
非常朴实无华的加写锁
先会判断现在的写标记位是不是0,是的话直接CAS加写锁
不然就通过acquireWrite进入队列
tryWriteLock()
public long tryWriteLock() {long s, next;return ((((s = state) & ABITS) == 0L &&U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?next : 0L);
}
也是非常朴实无华的尝试加写锁
写标记为0就CAS,不然就返回0
tryWriteLock(long time, TimeUnit unit)
public long tryWriteLock(long time, TimeUnit unit)throws InterruptedException {long nanos = unit.toNanos(time); //算出能用的时间if (!Thread.interrupted()) { //如果没被打断的话long next, deadline;if ((next = tryWriteLock()) != 0L) //拿到锁了,返回新的stampreturn next;if (nanos <= 0L) //能用的时间为负数或0,返回0return 0L;if ((deadline = System.nanoTime() + nanos) == 0L) //如果两者相加为0说明发生了溢出,就设成1兜底deadline = 1L;if ((next = acquireWrite(true, deadline)) != INTERRUPTED) //如果通过队列拿到锁了,而且没被中断,就返回return next;}throw new InterruptedException(); //被中断了,抛异常
}
带超时的写锁,会直接用acquireWrite(true, deadline)进入队列,然后根据是否超时和是否被中断来判断返回,没什么好讲的
tryUnlockWrite()
public boolean tryUnlockWrite() {long s; WNode h;if (((s = state) & WBIT) != 0L) { //有写锁state = (s += WBIT) == 0L ? ORIGIN : s; //版本号+1,如果成了0,说明溢出了,就变回ORIGIN,重新开始,因为如果返回0会被当成失败if ((h = whead) != null && h.status != 0)release(h); //唤醒合法的后继节点return true;}return false;
}
又是朴实无华的尝试释放锁
在try系方法中,0代表失败,所以在运行很多年后,可能会因为溢出重新变成0,那就要重置为ORIGIN,防止返回了0导致误以为失败
unlockWrite()
public void unlockWrite(long stamp) {WNode h;if (state != stamp || (stamp & WBIT) == 0L) //看看版本号一不一样,再判断写标记是不是1throw new IllegalMonitorStateException();state = (stamp += WBIT) == 0L ? ORIGIN : stamp; //进行解锁,为什么这样做参考上方if ((h = whead) != null && h.status != 0)release(h); //唤醒合法的后继节点
}
不在赘述
unlock()
public void unlock(long stamp) {long a = stamp & ABITS, m, s; WNode h; //a是后八位while (((s = state) & SBITS) == (stamp & SBITS)) { //看看版本号是不是一样if ((m = s & ABITS) == 0L) //如果后八位是0,这是乐观读的stamp,直接退出break;else if (m == WBIT) { //只有写锁,没有读锁if (a != m) //a != m 意味着中间肯定出现了读锁,这是不可能的,直接退出break;state = (s += WBIT) == 0L ? ORIGIN : s; //写锁就是本线程,CAS释放后唤醒可能的后继节点if ((h = whead) != null && h.status != 0)release(h);return;}else if (a == 0L || a >= WBIT) //如果即没有读锁也没有写锁,或者有写锁也有读锁break; //直接退出,不能在有写锁的情况下有读锁,stamp错误else if (m < RFULL) { //证明这是读锁,开始进行读锁的解锁if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) {if (m == RUNIT && (h = whead) != null && h.status != 0)release(h);return;}}else if (tryDecReaderOverflow(s) != 0L)return;}throw new IllegalMonitorStateException();
}
unlock是一个复合方法,既能解锁读锁,又能解锁写锁
会根据你传入的stamp判断这是读的stamp还是写的stamp
读的stamp写标记一定是0,写的读计数一定是0
unstampedUnlockWrite()
final void unstampedUnlockWrite() {WNode h; long s;if (((s = state) & WBIT) == 0L) //如果写标记为0抛异常throw new IllegalMonitorStateException();state = (s += WBIT) == 0L ? ORIGIN : s; //同上if ((h = whead) != null && h.status != 0)release(h);
}
相比读的来说非常简单,就是把state版本号+1,单线程就是简单
读,写方面介绍得差不多了,接下来就是重头戏:acquireWrite,acquireRead和cancelWaiter了
5.acquireRead(),acquireWrite(),cancelWaiter()
acquireRead()
这是stamplock最复杂的地方之一,我只会在这里高度概括一下其作用以及其机制,并不会贴源码来实际分析
acquireRead可分为4个部分:
- 1.快速尝试阶段:对于刚进入,发现头节点等于尾节点,那说明没有线程再排队,那就自旋拿锁,一旦自旋用完了,会看是如果写锁一直没被释放或者有别的线程也来快速通道抢锁,都会退回到写节点正常排队,不然的话会继续自旋
- 2.排队阶段:这里的节点会尝试在队列中排队:如果前驱是写节点就会挂在写节点之后,反之是读节点,就会堆叠到读节点上,到时候可以一并唤醒(读共享)
- 3.等待循环阶段:1.如果头节点是读,就会去帮忙唤醒头节点堆叠的其他读线程,反之就会park住,这里也会检测超时时间
- 4.队首抢锁阶段:自己到达队首,就会想第一阶段那样再次尝试抢锁:如果抢锁还是失败会给一点自旋补偿(取决于你是单核还是多核),还是不行的话就直接取消掉该节点,返回中断或者0
我们可以知道acquireRead的几下特点:
1.读节点可以堆叠多个读线程,会一并唤醒
2.中部的线程会帮助头节点来唤醒堆叠的读线程
3.自旋次数取决于是单核还是双核
4.对于自旋失败的头节点会直接取消掉
acquireWrite()
其实和acquireRead并没有太多的区别:
都是快速尝试,排队,等待循环自旋,队首抢锁
也都会帮助头结点唤醒堆叠的读线程
cancelWaiter()
这个和之前AQS取消节点的方式非常像:
首先会把自己标记成已取消,然后清理堆叠的线程:如果是读节点,会清理掉自己堆叠的线程
接着看自己是不是头节点,是的话会直接唤醒所有的没被取消的堆叠线程
然后取消自己:
找到合法前驱(如果有),连接到合法后驱上
取消完后,如果自己是头结点,会直接唤醒下一个节点
6.总结
总的来说,StampLock非常复杂,具体是在acquireWrite,cancelWaiter和acquireRead这三个方法上,极其复杂极其难度,极其冗长
其原理,作用还是不难读清楚
其更像是一个特化的锁结构,不像其他锁都是依托于AQS之中,他是自己创建了一个适合自己的CLH队列
导致了其在处理读大于写的场景下性能极其优秀,代价是不可重入,不支持条件队列等
接下来应该是最后一篇关于JUC锁的文章:Semaphore