当前位置: 首页 > news >正文

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(),固定为7

    state位运算参数:

  • 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

相关文章:

  • 具身系列——Double DQN算法实现CartPole游戏(强化学习)
  • 永磁同步电机控制算法--基于PI的位置伺服控制
  • STM32智能垃圾桶:四种控制模式实战开发
  • axi总线粗略学习
  • 方案精读:110页华为云数据中心解决方案技术方案【附全文阅读】
  • 【Trae+LucidCoder】三分钟编写专业Dashboard页面
  • 35、C# 中的反射(Reflection)
  • C++类与对象—下:夯实面向对象编程的阶梯
  • Python之学习笔记(六)
  • 统计 三个工作日内到期的数据
  • 【多线程】八、线程池
  • TS 字面量类型
  • [2025]MySQL的事务机制是什么样的?redolog,undolog、binog三种日志的区别?二阶段提交是什么?ACID怎么保证的?主从复制的过程?
  • Jasper and Stella: distillation of SOTA embedding models
  • Solr 与 传统数据库的核心区别
  • 学习黑客Linux 命令
  • Django框架介绍+安装
  • 工业元宇宙:从虚拟仿真到虚实共生
  • 【mathematica】常见命令
  • 【51单片机6位数码管显示时间与秒表】2022-5-8
  • 印度扩大对巴措施:封锁巴基斯坦名人账号、热门影像平台
  • 抗战回望16︱《青年生活》《革命青年》:抗战与青年
  • 中国企业转口贸易破局之道:出口国多元化,内外贸一体化
  • 德雷克海峡发生6.4级地震,震源深度10千米
  • AI世界的年轻人|他用影像大模型解决看病难题,“要做的研究还有很多”
  • 深交所修订创业板指数编制方案,引入ESG负面剔除机制