StampedLock实现概述
StampedLock与之前的ReentrantLock,ReentrantReadWriteLock使用队列同步列AQS实现有所不同,StampedLock的state改为了一个long型的变量,同时状态的设计也有所不同。同时由于没有使用AQSStampedLock直接在内部实现了同步等待队列,并且节点属性中有一个叫做cowait的分支用于标识另一个等待获取读状态的链。
StampedLock的读写状态设计
StampedLock采用了一个long型作为state,把这个64位的state的前7位作为读状态,第8位标识写状态,这也是为什么不支持重入的原因吧。
一些属性的定义:
//获取CPU的可用线程数量,用于确定自旋的时候循环次数
private static final int NCPU = Runtime.getRuntime().availableProcessors();
//根据NCPU确定自旋的次数限制(并不是一定这么多次,因为实际代码中是随机的)
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 LG_READERS = 7;
//一个读状态单位
private static final long RUNIT = 1L;
//写状态标识
private static final long WBIT = 1L << LG_READERS;
//读状态标识(前7位)
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; // note overlap with ABITS
//初始化状态
private static final long ORIGIN = WBIT << 1;
//中断标识
private static final long INTERRUPTED = 1L;
// 等待/取消
private static final int WAITING = -1;
private static final int CANCELLED = 1;
//读/写状态
private static final int RMODE = 0;
private static final int WMODE = 1;
//因为读状态只有7位很小,所以当超过了128之后将使用一个int变量来记录
private transient int readerOverflow;
看一下上面几关键的二进制表示:
------------------- ------------------- ------------------- -------------------
WBIT : 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1000 0000
RBITS : 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0111 1111
RFULL : 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0111 1110
ABITS : 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1111 1111
SBITS : 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000 0000
ORIGIN : 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 0000 0000
相对会比较直观的看出读写状态的标识。
没有写线程获取到了写状态只需判断:state < WBIT
读状态是否超出:(state & ABITS) < RFULL
获取读状态: state + RUNIT(或者readerOverflow + 1)
获取写状态: state + WBIT
释放读状态: state - RUNIT(或者readerOverflow - 1)
释放写状态: (s += WBIT) == 0L ? ORIGIN : s
是否为写锁: (state & WBIT) != 0L
是否为读锁: (state & RBITS) != 0L
源码
由于源码非常的复杂,这里适当分析一下。
node节点
static final class WNode {
volatile WNode prev;
volatile WNode next;
volatile WNode cowait; // list of linked readers
volatile Thread thread; // non-null while possibly parked
volatile int status; // 0, WAITING, or CANCELLED
final int mode; // RMODE or WMODE
WNode(int m, WNode p) { mode = m; prev = p; }
}
值得注意的就是这个cowait,用于链接等待获取读状态的节点。
获取读锁
public long readLock() {
long s = state, next;
//同步队列为空并且读状态没有超过最大值则直接尝试CAS设置同步状态
//否则调用acquireRead方法
return ((whead == wtail && (s & ABITS) < RFULL &&
U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
next : acquireRead(false, 0L));
}
再来看一个巨长的代码:
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;;) {
//直接尝试获取同步状态,成功返回stamp
//state & ABITS结果是state的前8位,RFULL为读状态的前7位的最大值-1
//m < WBIT说明没有写状态被占有
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) {
if (spins > 0) {
//有50%的几率 --spins,用于控制自旋的次数
if (LockSupport.nextSecondarySeed() >= 0)
--spins;
}
else {
if (spins == 0) {
WNode nh = whead, np = wtail;
//判断稳定性(有没有被修改),跳出循环
if ((nh == h && np == p) || (h = nh) != (p = np))
break;
}
//初始化spins
spins = SPINS;
}
}
}
}
//p(tail)为空初始化队列
if (p == null) { // initialize queue
//由于此时是有写线程占有同步状态所以用一个WMODE的节点放入队列
WNode hd = new WNode(WMODE, null);
//CAS插入,如果失败的话下次循环再次尝试
if (U.compareAndSwapObject(this, WHEAD, null, hd))
wtail = hd;
}
//初始化代表当前读线程的节点
else if (node == null)
node = new WNode(RMODE, p);
//head==tail或者队列tail.mode不为读状态,那么将当前线程的节点node加入到队列尾部并跳出外层循环
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;
}
}
//如果head!= tail说明队列中已经有线程在等待或者tail.mode是读状态RMODE,那么CAS方式将当前线程的节点node加入到tail节点的cowait链中
else if (!U.compareAndSwapObject(p, WCOWAIT,
node.cowait = p.cowait, node))
node.cowait = null;
//如果上面加入tail节点的cowait链中的CAS操作成功
else {
for (;;) {
WNode pp, c; Thread w;
//如果head不为空那么尝试去解放head的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);
//如果tail节点的前驱就是head或者head==tail或者tail节点的前驱是null
//也就是说当前node所在的节点(因为node可能在cowait链中)的前驱就是head或者head已经被释放了为null
if (h == (pp = p.prev) || h == p || pp == null) {
long m, s, ns;
//如果没有写状态被占有那么自旋方式尝试获取读状态,成功则返回stamp
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;
//如果tail的前驱是null或者head==tail或者tail已经被取消了(p.status > 0)
//直接将node置为null跳出循环,回到最开的for循环中去再次尝试获取同步状态
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;
//tail的前驱不是head或者当前只有写线程获取到同步状态
//判断稳定性
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);
}
}
}
}
//如果队列中没有节点或者tail的mode是WMODE写状态,那么node被加入到队列的tail之后进入这个循环
for (int spins = -1;;) {
WNode h, np, pp; int ps;
//如果p(node的前驱节点)就是head,那么自旋方式尝试获取同步状态
if ((h = whead) == p) {
//第一次循环
if (spins < 0)
spins = HEAD_SPINS;
else if (spins < MAX_HEAD_SPINS)
spins <<= 1;
for (int k = spins;;) { // spin at head
long m, s, ns;
//自旋方式尝试获取同步状态
//获取成功的话将node设置为head并解放node的cowait链中的节点并返回stamp
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;
while ((c = node.cowait) != null) {
if (U.compareAndSwapObject(node, WCOWAIT,
c, c.cowait) &&
(w = c.thread) != null)
U.unpark(w);
}
return ns;
}
//如果有写线程获取到了同步状态(因为可能有写线程闯入)那么随机的--k控制循环次数
else if (m >= WBIT &&
LockSupport.nextSecondarySeed() >= 0 && --k <= 0)
break;
}
}
//如果head不为null,解放head的cowait链中的节点
else if (h != null) {
WNode c; Thread w;
while ((c = h.cowait) != 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
}
//尝试设tail的状态位WAITING表示后面还有等待的节点
else if ((ps = p.status) == 0)
U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
//如果tail已经取消了
else if (ps == CANCELLED) {
if ((pp = p.prev) != null) {
node.prev = pp;
pp.next = node;
}
}
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);
}
}
}
}
基本流程就是先判断同步队列是否为空,如果为空那么尝试获取读状态,同时如果此时写状态被占有的话还是会根据spins的值随机的自旋一定的时间如果还是没获取到则跳出自旋进入外层的循环。如果不为空说明已经有别的线程在排队了自己肯定是没戏了,那么开始检查是否需要初始化,如果没有初始化则构造一个WMODE的节点作为头节点。此时构造当前线程的节点node尝试加入同步队列,加入的方式有两种,一种是如果队列的tail是WMODE或者队列的head==tail,那么直接加入队列的尾部,并跳出外层循环,一种是加入tail节点的cowait的链中。并继续执行。
在最后一个for循环中,节点的自旋限制为先驱节点就是头节点,并且自旋同样不是无休止的,二十通过一个spins的值来控制,并且是相对随机的。同时在cowait的上面等待的节点都是RMODE,所以如果cowai节点对应的同步队列中的节点一旦解放那就意味着这个链上的所有节点都会被解放。
释放读锁
释放读锁相对要简单许多
public void unlockRead(long stamp) {
long s, m; WNode h;
for (;;) {
//通过传来的stamp(也就是上一次的state)来比较
//state & SBITS之后将抹去前7位以外的部分只剩下读状态
if (((s = state) & SBITS) != (stamp & SBITS) ||
(stamp & ABITS) == 0L || (m = s & ABITS) == 0L || m == WBIT)
throw new IllegalMonitorStateException();
//读状态是否超过最大值
if (m < RFULL) {
//设置state
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;
}
}
private void release(WNode h) {
if (h != null) {
WNode q; Thread w;
//将h的状态设为0
U.compareAndSwapInt(h, WSTATUS, WAITING, 0);
//从tail开始找到距离h最近的status <= 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);
}
}
获取写锁
这里的写锁是会被读锁阻塞的(因为乐观读根本就没获取同步状态,后面会讲),所以直接判断(s = state) & ABITS) == 0L(是否有锁,也就是前8位的值)
public long writeLock() {
long s, next;
//state & ABITS只剩下前8位,因此如果有别的写锁或者读锁存在将失败
//尝试CAS获取写锁
//失败调用acquireWrite
return ((((s = state) & ABITS) == 0L &&
U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
next : acquireWrite(false, 0L));
}
再看一个长的:
private long acquireWrite(boolean interruptible, long deadline) {
WNode node = null, p;
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)
spins = (m == WBIT && wtail == whead) ? SPINS : 0;
else if (spins > 0) {
if (LockSupport.nextSecondarySeed() >= 0)
--spins;
}
else if ((p = wtail) == 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(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;;) {
WNode h, np, pp; int ps;
if ((h = whead) == p) {
if (spins < 0)
spins = HEAD_SPINS;
else if (spins < MAX_HEAD_SPINS)
spins <<= 1;
for (int k = spins;;) { // spin at head
long s, ns;
if (((s = state) & ABITS) == 0L) {
if (U.compareAndSwapLong(this, STATE, s,
ns = s + WBIT)) {
whead = node;
node.prev = null;
return ns;
}
}
else if (LockSupport.nextSecondarySeed() >= 0 &&
--k <= 0)
break;
}
}
else if (h != null) { // help release stale waiters
WNode c; Thread w;
while ((c = h.cowait) != 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
}
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();
U.putObject(wt, PARKBLOCKER, this);
node.thread = wt;
if (p.status < 0 && (p != h || (state & ABITS) != 0L) &&
whead == h && node.prev == p)
U.park(false, time); // emulate LockSupport.park
node.thread = null;
U.putObject(wt, PARKBLOCKER, null);
if (interruptible && Thread.interrupted())
return cancelWaiter(node, node, true);
}
}
}
}
写状态的获取基本和读一样,区别在于写状态获取的时候根本就没有去判断同步队列里面是否有节点,而且尝试获取写状态的条件是(s = state) & ABITS) == 0L,也就是说要没有任何的其他锁占用的情况下才会去CAS尝试获取写状态。同时如果获取失败加入同步队列的时候只会直接加入同步队列的尾部,不会加入cowait链。这也说明了StampedLock的写是无条件去获取锁。
写锁的释放
public void unlockWrite(long stamp) {
WNode h;
//因为写锁是独占式的所以可以简单判断state != stamp
if (state != stamp || (stamp & WBIT) == 0L)
throw new IllegalMonitorStateException();
state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
if ((h = whead) != null && h.status != 0)
release(h);
}
/*
*这里的(stamp += WBIT) == 0L ? ORIGIN : stamp解释:
*假设stamp为:ORIGIN + WBIT(第一次获取了写锁的状态)
*0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 1000 0000
*那么加上一个WBIT之后位
*0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0010 0000 0000
*此时第八位已经为0,表示已经释放了写锁
*但是随着这样累加上去可能最后会溢出结果64位全部为0,所以如果这种情况就置为ORIGIN
/*
读/写锁释放
StampedLock还提供了一个unlock方法,该方法可以自己判断并释放相应的锁。
public void unlock(long stamp) {
long a = stamp & ABITS, m, s; WNode h;
//判断stamp是否符合
while (((s = state) & SBITS) == (stamp & SBITS)) {
//如果一个锁都没有
if ((m = s & ABITS) == 0L)
break;
//如果是写锁则释放写锁
else if (m == WBIT) {
if (a != m)
break;
state = (s += WBIT) == 0L ? ORIGIN : s;
if ((h = whead) != null && h.status != 0)
release(h);
return;
}
else if (a == 0L || a >= WBIT)
break;
//如果是读锁并且没有overflow
else if (m < RFULL) {
if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) {
if (m == RUNIT && (h = whead) != null && h.status != 0)
release(h);
return;
}
}
//如果是读锁并且overflow
else if (tryDecReaderOverflow(s) != 0L)
return;
}
throw new IllegalMonitorStateException();
}
因为读锁和写锁是互斥的,而且当前线程只能获取这其中的一把锁,所以只需判断是否是写锁即可。
乐观读
上面讲了那么多读写锁的内容,最关键的乐观读又是怎么获取锁的呢,实际上乐观读根本就没有获取锁。只是单纯的将当前的state作为stamped返回,然后读取之后再调用validate看stamp和state是否匹配,不匹配说明有别的线程进行了修改,需要程序员决定下一步的策略(使用读锁或者忽略等等)。
public long tryOptimisticRead() {
long s;
//当前没有读线程占用锁则返回state
return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}
public boolean validate(long stamp) {
//表示该方法之前的所有load操作在内存屏障之前完成(防止重排序)
U.loadFence();
//验证
return (stamp & SBITS) == (state & SBITS);
}
小结
对于StampedLock源码来说过于复杂和追求代码的简洁,所以很难看的明吧(包括我自己),但是对于读写锁的使用的来说变得更加灵活,但是有几点已知的问题需要注意:一个是StampedLock是不可重入的,二一个是StampedLock对于带有中断的线程的处理可能导致CPU暴涨。三一个是StampedLock的所有的try开头的获取都将直接尝试获取锁。最后StampedLock适用于读多写少的场景。