StampedLock
最近看并发工具类的遇到这个玩意,据说效率比ReentrantLock高,咱也没用过,闲得慌,源码先扒了再说,不定期更新。
锁状态变量
既然是类似读写锁,肯定也有变量用于记录锁状态,相对于读写锁使用Int,StampedLock
中使Long类型(64位)表示锁状态,其中第8位表示写锁持有状态,低7位用于表示持有读锁的线程数。(目前还未搞懂为啥用Long,待后面研究)
private static final long RUNIT = 1L; //读锁线程增加时,
//获取写锁状态的掩码:1000 000
private static final long WBIT = 1L << LG_READERS;
//获取读锁状态的掩码:0111 1111
private static final long RBITS = WBIT - 1L;
//读锁持有数量的上限
private static final long RFULL = RBITS - 1L;
//获取锁的状态,用于判断是否有线程持有读锁或写锁
private static final long ABITS = RBITS | WBIT;
//RBITS 取反
private static final long SBITS = ~RBITS; // note overlap with ABITS
写锁获取
public long writeLock() {
long s, next;
//如果没有任何线程持有锁,CAS尝试添加写锁状态
//如果失败,则acquireWrite方法中自旋尝试获锁,再次失败入队
return ((((s = state) & ABITS) == 0L &&
U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
next : acquireWrite(false, 0L));
}
acquireWrite
(方法巨长,真难看),该方法其实就是自旋获取锁,获取不到要么协助唤醒,要不就挂起,过程如下:
1、刚开始,啥都不管,我就要去拿写锁,不停地自旋
2、自旋完了还是搞不赢,只能先入个队(尾插)观望一波,注意,只是入个队,没挂起线程
3、那入队后观望啥呢?肯定是看看自己排在哪里呀,如果是排在队列头结点的后面,那机会又来了,开始不要命地自旋去获锁。(因为头结点是一个拿到锁的结点,后继结点作为二把手肯定要开始抢锁了)
4、如果发现自己排在老后面无希望拿到写锁怎么办?躺平也不是事,得想办法加快自己获取锁的进程;如果头结点是获取读锁线程的结点,如下图中ReaderNode1那种,是个栈,那就要帮忙唤醒整队列中挂起的线程,以提高自己获取锁的速度。
5、那如果条件还是不满足怎么办?那就只能挂起当前线程,老老实实等待了(注意会响应超时和中断)
源码分析如下:
private long acquireWrite(boolean interruptible, long deadline) {
WNode node = null, p;
//这个自旋用来尝试获取写锁,自旋结束后,尾插法入CLH队列
for (int spins = -1;;) { // spin while enqueuing
long m, s, ns;
// 通过自旋不断地尝试获取写锁,成功则返回当前的锁状态
if ((m = (s = state) & ABITS) == 0L) {
if (U.compareAndSwapLong(this, STATE, s, ns = s + WBIT))
return ns;
}
else if (spins < 0)
//写锁被其它线程占有,且等待队列为空(头等于尾)时,设置自旋次数
//多核2的6次方,单核为0
spins = (m == WBIT && wtail == whead) ? SPINS : 0;
else if (spins > 0) {
// 通过随机数来递减自旋次数
if (LockSupport.nextSecondarySeed() >= 0)
--spins;
}
//当自旋次数为0的时候,开始初始化等待队列:CLH队列
//队列初始状态:头结点和尾结点指向同一个结点
else if ((p = wtail) == null) {
WNode hd = new WNode(WMODE, null);
if (U.compareAndSwapObject(this, WHEAD, null, hd))
wtail = hd;
}
//下面三个if就是将node采用尾插法插入等待队列,并退出自旋
//注意,此node.thread仍为null,线程还未挂起
else if (node == null)
node = new WNode(WMODE, p);
else if (node.prev != p)
node.prev = p;
else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
p.next = node;
break;
}
}
for (int spins = -1;;) {
// h头结点,np,当前结点前置结点,pp当前结点的前置结点的前置结点,ps前置结点状态:0初始状态,-1等待状态,1取消状态
WNode h, np, pp; int ps;
//当前node的前置结点就是头结点,那么当前结点可以去尝试去获取锁了(头结点是拿到了锁的)
//类似AQSd的同步结点:头结点为持锁结点,每次线程释放锁会唤醒头结点的后继结点
if ((h = whead) == p) {
if (spins < 0)
spins = HEAD_SPINS; // 初始化自旋次数为2的10次方(CPU多核)
// 当上次自旋spins 次后,仍未获取写锁,将自旋次数增大2倍,继续自旋
else if (spins < MAX_HEAD_SPINS)
spins <<= 1;
//作为头结点的后继结点,会一直自旋获取写锁,没必要挂起
for (int k = spins;;) { // spin at head
long s, ns;
//只要现在锁状态为0,那我就尝试给它添加写锁状态
if (((s = state) & ABITS) == 0L) {
if (U.compareAndSwapLong(this, STATE, s,
ns = s + WBIT)) {
//获取写锁成功,当前node成为新的头结点
whead = node;
node.prev = null;
return ns;
}
}
// 当自旋计数为0时,退出当前循环,spins 扩大2倍再次自旋
else if (LockSupport.nextSecondarySeed() >= 0 &&
--k <= 0)
break;
}
}
// 如果当前队列已经初始化,h!=null,且当前结点不是头结点后继结点
// 那么当前结点不能获取锁,闲着也是闲着,就帮忙唤醒挂起等待的结点
else if (h != null) {
WNode c; Thread w;
//如果头结点是读结点cowait栈(CLH队列中,前面是写结点,后面如果是连续的的读结点,不会在CLH中往后排,而是第一读结点为头,拉起一个链表,有点类似HashMap解决Hash冲突时那种结构)
while ((c = h.cowait) != null) {
//cas,是c指向下一个读结点,如果该结点线程不为null,则唤醒
if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
(w = c.thread) != null)
U.unpark(w);
}
}
// 队列头一动不动,锁压根没人释放
// 这个时候,获取锁啥的肯定没辙,没事干,只能看看队尾有啥子变动没
if (whead == h) {
if ((np = node.prev) != p) {
if (np != null)
(p = np).next = node; // stale
}
//当前线程Node的前置结点状态如果为0,设置为等待状态(前面提过0初始状态,-1等待状态,1取消状态)
else if ((ps = p.status) == 0)
U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
// 如果前置结点为取消状态,就踢出队列
else if (ps == CANCELLED) {
if ((pp = p.prev) != null) {
node.prev = pp;
pp.next = node;
}
}
else {
long time; // 0 argument to park means no timeout
if (deadline == 0L)
time = 0L;
// 如果有設置等待时间,时间到了就取消等待
else if ((time = deadline - System.nanoTime()) <= 0L)
return cancelWaiter(node, node, false);
Thread wt = Thread.currentThread();
// jdk bug,解决jstack打印的时候,不显示当前线程挂在哪个对象锁上
U.putObject(wt, PARKBLOCKER, this);
node.thread = wt;
// 当前结点的前驱结点为等待状态
//前驱结点不是头结点,或锁状态不为0
//或者队列暂无变化
if (p.status < 0 && (p != h || (state & ABITS) != 0L) &&
whead == h && node.prev == p)
//挂起线程
U.park(false, time); // emulate LockSupport.park
// 结束挂起后,清理掉node中持有的当前线程引用
node.thread = null;
U.putObject(wt, PARKBLOCKER, null);
//如果线程被唤醒是因为中断,则取消等待
if (interruptible && Thread.interrupted())
return cancelWaiter(node, node, true);
}
}
}
}
从上面方法可以看出,不到万不得已,是不会选择挂起线程去等待被唤醒,而是不断尝试通过CAS去拿锁,这也是高性能的由来吧,不过,感觉这种大量的自旋有的时候也是对CPU资源浪费。
当前线程获锁失败时需要等,但支持超时取消和响应中断。cancelWaiter
这个方法做了两件事,将队列中为取消状态的结点(status >0)踢出队列,释放等待队列头结点(status 改为0),唤醒头结点的后继结点。我们继续看看它如何实现:
private long cancelWaiter(WNode node, WNode group, boolean interrupted) {
if (node != null && group != null) {
Thread w;
// node为要取消的结点,状态改为CANCELLED
node.status = CANCELLED;
// 如果group结点是cowait栈结构,则移除栈中被取消的结点
for (WNode p = group, q; (q = p.cowait) != null;) {
if (q.status == CANCELLED) {
U.compareAndSwapObject(p, WCOWAIT, q, q.cowait);
p = group; // restart
}
else
p = q;
}
//acquireWrite方法调用的时候,这个是为true
if (group == node) {
// 唤醒cowait栈中未取消的等待结点(这些结点都是读模式也就是获取读锁线程)
// 因为它们的头头group要被取消了,不唤醒可能就没人唤醒它们了
for (WNode r = group.cowait; r != null; r = r.cowait) {
if ((w = r.thread) != null)
U.unpark(w); // wake up uncancelled co-waiters
}
for (WNode pred = node.prev; pred != null; ) { // unsplice
WNode succ, pp;
//找到node的一个未被取消的后继结点
while ((succ = node.next) == null ||
succ.status == CANCELLED) {
WNode q = null; // find successor the slow way
//这种和AQS里面一样,从尾部往前遍历找node未取消的后继结点,
//结点插入时,因prev指针比next先设置,所以next为空时,后继结点未必不存在,只是未来得及设置next而已
for (WNode t = wtail; t != null && t != node; t = t.prev)
if (t.status != CANCELLED)
q = t; // don't link if succ cancelled
//确保node的后继结点是有效的(未取消)
if (succ == q || // ensure accurate successor
U.compareAndSwapObject(node, WNEXT,
succ, succ = q)) {
// 如果node为尾结点,它被取消,那么CAS修改尾指向node的前驱结点
if (succ == null && node == wtail)
U.compareAndSwapObject(this, WTAIL, node, pred);
break;
}
}
// 将node踢出队列,node的前驱结点指向它的后继结点
// 有一点不理解,为什么没有取消node的prev指针?
if (pred.next == node) // unsplice pred link
U.compareAndSwapObject(pred, WNEXT, node, succ);
//唤醒node的后继结点,因为node被取消,不去唤醒,就没人去唤醒后继结点succ了
if (succ != null && (w = succ.thread) != null) {
succ.thread = null;
U.unpark(w); // wake up succ to observe new pred
}
//这里几行代码是将node取消的前驱结点踢出,找到有效前驱结点。
if (pred.status != CANCELLED || (pp = pred.prev) == null)
break;
node.prev = pp; // repeat if new pred wrong/cancelled
U.compareAndSwapObject(pp, WNEXT, pred, succ);
pred = pp;
}
}
}
WNode h; // Possibly release first waiter
//这段代码就是找到头结点的有效后继结点并唤醒,因为头结点是拿到了锁的
while ((h = whead) != null) {
long s; WNode q; // similar to release() but check eligibility
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 (h == whead) {
//后继结点存在,头结点既不是取消状态也不是等待状态,写锁已释放
//后面两个条件没理解,看来读锁再来
if (q != null && h.status == 0 &&
((s = state) & ABITS) != WBIT && // waiter is eligible
(s == 0L || q.mode == RMODE))
release(h);
break;
}
}
return (interrupted || Thread.interrupted()) ? INTERRUPTED : 0L;
}
唤醒头结点的后继结点:
private void release(WNode h) {
if (h != null) {
WNode q; Thread w;
//取消头结点的等待状态
U.compareAndSwapInt(h, WSTATUS, WAITING, 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);
}
}
写锁释放
写锁释放就比较简单,涉及到动作没获取时那么夸张,就两点:1、清除邮戳stamp中写锁占有状态,2、唤醒队列中头结点的后继结点去拿锁。
public void unlockWrite(long stamp) {
WNode h;
// 加写锁的时候,会返回一个邮戳stamp,里面包含了写锁状态
// 如果邮戳状态变了,或者写锁未被占用,报错
if (state != stamp || (stamp & WBIT) == 0L)
throw new IllegalMonitorStateException();
// 写锁在stamp的第八位,加上WBIT就是将第八位变为0,清除写锁,同时stamp会进一位
state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
//释放锁后,唤醒队列中头结点的后继结点
if ((h = whead) != null && h.status != 0)
release(h);
}
到此写锁看完了,我们发现写锁还是独占模式,而且不像ReentrantReadWriteLock
支持锁重入,也不支持Condition
进行精细化地控制锁。当写锁被占的时候,后面线程想获取写锁,经过大量地自旋才会挂起入等待队列。ReentrantReadWriteLock
虽然获取不到锁也会自旋等待,但是StampedLock自旋次数更多,尽最大可能去避免线程挂起。
读锁的获取
读锁在入队时,发现尾结点是WMODE(写模式)时,如下图,如果WNODE1是写模式,且是队尾时,获取读锁线程阻塞入队,WNODE2就直接插入到后面成为新的队尾,为读模式;
如果又来了个想获取读锁的线程,它发现队尾时读模式,那么它就不会再加入到WNode2后面形成新队尾,而是以WNode2为头形成一个栈,如图中cowait1;
如果又来了一个线程还是拿读锁,那么它继续入栈,如图中cowait2
这样设计,将连续读线程放到一个栈中,有一个好处:前面竞争写锁线程,出队后,可以同时唤醒栈中所有线程去获取写锁,效率高
public long readLock() {
long s = state, next; // bypass acquireRead on common uncontended case
// 如果队列中没有排队线程,读锁获取没有达到上限,那就CAS 读锁数量加1
// 否则又是通过acquireRead自旋获取锁,失败后入队,原理上和写锁大差不差
return ((whead == wtail && (s & ABITS) < RFULL &&
U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
next : acquireRead(false, 0L));
}
acquireRead
方法涉及到自旋获取锁,失败后入队或取消操作,以及对头结点的cowait栈的挂起线程协助唤醒操作,总之巨鸡儿长,能一行一行看完的都是勇士o.o。
private long acquireRead(boolean interruptible, long deadline) {
WNode node = null, p;
for (int spins = -1;;) {
WNode h;
// 头等于尾,队列为空,自旋获取读锁
if ((h = whead) == (p = wtail)) {
for (long m, s, ns;;) {
if ((m = (s = state) & ABITS) < RFULL ?
U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
(m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L))
return ns;
//写锁被占
else if (m >= WBIT) {
//自旋次数大于0,继续自旋,随机扣除自旋次数
if (spins > 0) {
if (LockSupport.nextSecondarySeed() >= 0)
--spins;
}
else {
//自旋次数用完,如果队列还是没变,或者队列变了,但是还有等待结点,就不再自旋
if (spins == 0) {
WNode nh = whead, np = wtail;
if ((nh == h && np == p) || (h = nh) != (p = np))
break;
}
//自旋次数小于0,则初始化它
spins = SPINS;
}
}
}
}
// 一阶段自旋失败后,就为入队作准备了
//如果队列未初始化就初始化它
if (p == null) { // initialize queue
WNode hd = new WNode(WMODE, null);
if (U.compareAndSwapObject(this, WHEAD, null, hd))
wtail = hd;
}
// 初始化当前结点
else if (node == null)
node = new WNode(RMODE, p);
//队列为空或者队尾不是读模式,则直接插入队列
else if (h == p || p.mode != RMODE) {
if (node.prev != p)
node.prev = p;
else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
p.next = node;
break;
}
}
// 走到这里,说明队尾为读模式,此时新结点加入到队尾结点的栈中
//p结点不动,新增结点每次都是插在p的后面,作为后继结点
// p->wc1 ==> p->wc2->wc1
//从这里我们可以看出,只有前置结点为写模式,当前node才能加到CLH队列中,否则,就加入到前置结点的栈中
else if (!U.compareAndSwapObject(p, WCOWAIT,
node.cowait = p.cowait, node))
node.cowait = null;
// CAS 成功,当前结点成功入栈,注意下面这个自旋专门处理入栈后的读结点,
// 因为入CLH队列的读结点在上面直接break了。会走下下面那个自旋
//这个自旋作用:
//1、唤醒头结点上cowait栈的挂起线程,
//2、决定当前线程结点是挂起还是取消(当然在这之前还有一次获锁机会)
else {
for (;;) {
WNode pp, c; Thread w;
// 如果头结点存在cowait栈,那么就通过外层自旋全部唤醒它
if ((h = whead) != null && (c = h.cowait) != null &&
U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
(w = c.thread) != null) // help release
U.unpark(w);
//注意:这里的p仍然指向的是尾结点,因为当前结点只是入了尾结点所在的栈,不是加入到队列中,替换为尾结点
//尾结点前置结点等于头说明队列只有一个结点,或者队列为空,此时当前node应该尝试一下去获锁
if (h == (pp = p.prev) || h == p || pp == null) {
long m, s, ns;
do {
if ((m = (s = state) & ABITS) < RFULL ?
U.compareAndSwapLong(this, STATE, s,
ns = s + RUNIT) :
(m < WBIT &&
(ns = tryIncReaderOverflow(s)) != 0L))
return ns;
} while (m < WBIT);// 循环条件是写锁未被占用
}
//上面获锁失败,且队列无变化,此时获锁无望,考虑挂起还是搞别的啥事了
if (whead == h && p.prev == pp) {
long time;
//前两个条件用于判断队列没有等到线程了,则当前线程又有了资格获锁,没必要排队了
//后一个条件是队尾被取消了,因为当前node是挂在队尾为头的栈上啊,队尾一取消,也挂不住了
//因此node置为null,退出当前循环,重新去外层经历循环获锁,入队等操作
if (pp == null || h == p || p.status > 0) {
node = null; // throw away
break;
}
// 如果设置了超时时间,判断超时取消当前结点排队
if (deadline == 0L)
time = 0L;
else if ((time = deadline - System.nanoTime()) <= 0L)
return cancelWaiter(node, p, false);
Thread wt = Thread.currentThread();
U.putObject(wt, PARKBLOCKER, this);
node.thread = wt;
//此处线程判断线程是否该挂起,这巴拉巴拉条件,可见作者是真的不想让线程挂起o.o
//在队列无变化的前提下,CLH队列有其它结点在排队,或者写锁还被占用
if ((h != pp || (state & ABITS) == WBIT) &&
whead == h && p.prev == pp)
U.park(false, time);
node.thread = null;
U.putObject(wt, PARKBLOCKER, null);
//响应中断,当前线程因为中断被唤醒,会被取消排队
if (interruptible && Thread.interrupted())
return cancelWaiter(node, p, true);
}
}
}
}
//能走到这里,是上面大循环中,读模式结点成功加入到CLH队列后,break才到这的
//这个自旋就是为了处理CLH队列上刚插入读模式结点的,而刚插入到栈上的读模式结点,在上个循环里面的一个自旋中已处理过了
for (int spins = -1;;) {
WNode h, np, pp; int ps;
//头结点等于当前结点的前驱结点,说明队列中只有当前结点在排队,再次尝试获读锁
//注意啊:这里的p和上个循环的p所指的不一样,这里是指向尾结点的前置结点
//因为能走到这里,说明node已经入了CLH队列成为了新的尾结点,p.next=node
if ((h = whead) == p) {
//这部分代码和写锁里面差不多,初始化自旋值
if (spins < 0)
spins = HEAD_SPINS;
// 如果自旋spins次数后,仍然无法获取锁,那就将它扩大2倍
else if (spins < MAX_HEAD_SPINS)
spins <<= 1;
//这里开始自旋获锁了
for (int k = spins;;) { // spin at head
long m, s, ns;
if ((m = (s = state) & ABITS) < RFULL ?
U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
(m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L)) {
WNode c; Thread w;
whead = node;
node.prev = null;
//当前node获取锁成功后,如果是存在cowait栈,就唤醒栈上等待的线程
//因为读锁是可以多个线程去获取的,因写锁被占,其它获取读锁线程会加入到栈中挂起
while ((c = node.cowait) != null) {
if (U.compareAndSwapObject(node, WCOWAIT,
c, c.cowait) &&
(w = c.thread) != null)
U.unpark(w);
}
return ns;
}
//或锁失败,写锁仍被占,就随机自旋次数减一,为0时退出循环
else if (m >= WBIT &&
LockSupport.nextSecondarySeed() >= 0 && --k <= 0)
break;
}
}
// 说明队列中有多个结点在排队
else if (h != null) {
WNode c; Thread w;
//看看头结点中是否有cowait栈,有的话,帮忙唤醒,提高效率
while ((c = h.cowait) != null) {
if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
(w = c.thread) != null)
U.unpark(w);
}
}
//头结点没变化
if (whead == h) {
//但node的前置结点变了,这说明了旧的前置结点被取消了,之前在获取写锁中,我们也可以看到多个清理被取消结点的过程
if ((np = node.prev) != p) {
if (np != null)
//设置新前置结点的next指针
(p = np).next = node; // stale
}
//如果前置结点状态还不是等待状态,也不是取消状态,那就把它设置为等待状态
// 这样当前node才能安心挂起,到时候有人唤醒它
//类似于AQS中,CLH队列中结点的线程在挂起前,会给前置结点设置singal状态一样
// 都是为了让前置结点在释放锁的时候,唤醒自己
else if ((ps = p.status) == 0)
U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
//如果前置结点为取消状态,那就得找到一个未取消的结点作为新的前置结点
else if (ps == CANCELLED) {
if ((pp = p.prev) != null) {
node.prev = pp;
pp.next = node;
}
}
// 这个时候,终于要处理当前线程是挂起还是响应超时或中断取消了
// 都是一样的代码,没必要讲了,能看到这里,你也是牛逼大大的o.o
else {
long time;
if (deadline == 0L)
time = 0L;
else if ((time = deadline - System.nanoTime()) <= 0L)
return cancelWaiter(node, node, false);
Thread wt = Thread.currentThread();
U.putObject(wt, PARKBLOCKER, this);
node.thread = wt;
if (p.status < 0 &&
(p != h || (state & ABITS) == WBIT) &&
whead == h && node.prev == p)
U.park(false, time);
node.thread = null;
U.putObject(wt, PARKBLOCKER, null);
if (interruptible && Thread.interrupted())
return cancelWaiter(node, node, true);
}
}
}
}
读锁的释放:
public void unlockRead(long stamp) {
long s, m; WNode h;
//因为涉及到共享变量state的操作,不想加锁,那就老老实实CAS+自旋
for (;;) {
// 罗里吧嗦地校验邮戳的状态是否已变化,以及是否持有锁,写锁是否还存在
// 因为存在写锁时,不可能有读锁,所以释放不存在锁要报错
if (((s = state) & SBITS) != (stamp & SBITS) ||
(stamp & ABITS) == 0L || (m = s & ABITS) == 0L || m == WBIT)
throw new IllegalMonitorStateException();
// 如果未超过读锁数量上限
// 获取读锁时,若超过数量上限,不会拒绝,用readerOverflow来记录额外的读锁数
if (m < RFULL) {
if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) {
// 所有读锁释放完了才会唤醒
// 因为在前介绍的CLH队列中,读模式结点后面必然是写模式结点(连续的读模式结点会加到cowait栈中)
// 既然是写模式结点,写读互斥,那么只有读锁释放完了,才能拿到写锁,所以提前唤醒它没得意义
if (m == RUNIT && (h = whead) != null && h.status != 0)
release(h);
break;
}
}
// 处理读锁超过上限时,锁释放
else if (tryDecReaderOverflow(s) != 0L)
break;
}
}
tryDecReaderOverflow
这个方法用于释放时,获取的读锁已经超过上限的场景。
说句实话,我不大理解读锁超过上限后,为啥还搞个readerOverflow
这个玩意来记录超出的锁,毕竟你邮戳状态可是Long型的啊,64位啊,又不像AQS,锁状态是Int型的,32位不够用,为啥不直接把读锁从低7位阔大呢?StampedLock
的state前面好多位都没有用到,希望有懂的指点一下。
还有一点不理解,为什么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;
// 超出的读锁由readerOverflow计数,释放时减一
if ((r = readerOverflow) > 0) {
readerOverflow = r - 1;
next = s;
}
else
next = s - RUNIT;
state = next;
return next;
}
}
//根据线程随机数与上7,如果等于0,就线程让步
// 为啥要让啊?让给谁啊?莫名其妙的,看不懂
else if ((LockSupport.nextSecondarySeed() &
OVERFLOW_YIELD_RATE) == 0)
Thread.yield();
return 0L;
}
乐观读锁
前面说的读锁获取都是悲观读锁啊,与写锁时互斥的,持有读锁的时候,不能获取写锁,持有写锁的时候会阻塞获取读锁。乐观锁就不一样,适用于读多写少的场景,根据tryOptimisticRead
的注释,可以了解到,它必须和boolean validate(long stamp)
方法一起用才行,不然那就是瞎乐观了啊!
获取乐观锁时,如果存在写锁,那么乐观锁是无效的,需要用validate(long stamp)
验证,但是持有悲观读锁时,不影响乐观锁获取,获取乐观锁时,不影响写锁获取,只不过写锁获取后,stamp肯定是变了的,乐观锁也会失效。
public long tryOptimisticRead() {
long s;
// 获取乐观锁,写锁不能被占用
return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}
//用来验证tryOptimisticRead中返回的邮戳是否变化,即验证乐观锁是否有效
public boolean validate(long stamp) {
U.loadFence();
return (stamp & SBITS) == (state & SBITS);
}
StampedLock锁转换方法(简单翻译一下注释了)
tryConvertToReadLock(long stamp)
转为读锁
如果stamp记录的是写锁,则释放写锁,去获取读锁
如果stamp记录的是读锁,那就直接返回stamp
如果乐观读锁,当读锁可获取时,获取读锁,返回stamp
tryConvertToWriteLock(long stamp)
转为写锁
如果stamp记录的是读锁,则写锁能获取的情况下,释放读锁,获取写锁
如果stamp记录的是写锁,那就直接返回stamp
如果乐观读锁,当写锁可获取时,获取写锁,返回stamp
tryConvertToOptimisticRead(long stamp)
转为写锁
如果stamp记录的是其它锁,释放锁,获取乐观锁
如果乐观锁,那就直接返回stamp
小总
扯了这么多,发现StampedLock
性能高,是通过无锁,CAS+大量自旋来得到的,牺牲了CPU性能,这一点是所有高性能工具的共同点,同时它尽最大可能避免线程的挂起,这个很耗时。StampedLock
的写锁和悲观读是互斥,但是它和ReentrantReadWriteLock
相比还是有很多不同:
StampedLock
不支持重入锁StampedLock
不支持Condition,精细化操作锁StampedLock
支持不同锁之间的转换StampedLock
支持乐观读StampedLock
未基于AQS实现,等待队列是CLH队列+cowait栈构成
虽然有很多不同,但是大致思想:比如队列唤醒机制,以及队列结点取消操作等,都和AQS是大差不差的。
告辞!!!!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。