StampedLock详解
StampedLock简介
StampedLock是比ReentrantReadWriteLock更快的一种锁,支持乐观读、悲观读锁和写锁。和ReentrantReadWriteLock不同的是,StampedLock支持多个线程申请乐观读的同时,还允许一个线程申请写锁。
乐观读并不加锁
StampedLock的底层并不是基于AQS的。
StampedLock示例
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) { // an exclusively locked method
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
double distanceFromOrigin() { // A read-only method
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY currentY);
}
void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) {
long ws = sl.tryConvertToWriteLock(stamp);
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
}
else {
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}
}}
StampedLock源码分析
StampedLock也是通过一个int变量state、一个队列来实现的
state
state的默认值是256 1 0000 0000
state是一个int变量,总共有32位
前24位表示版本号、低8位表示锁。
低8位的第1位表示是否为写锁,1表示写锁、0表示没有写锁
剩下7位表示悲观读锁的个数
WNode
队列中的节点对应的是WNode
static final class WNode {
//前继节点
volatile WNode prev;
//后继节点
volatile WNode next;
//悲观读锁对应的栈
volatile WNode cowait;
//对应的线程
volatile Thread thread;
//节点的状态:取消CANCEL 1、等待WATING -1。
volatile int status;
//节点类型:读锁、写锁
final int mode;
WNode(int m, WNode p) { mode = m; prev = p; }
}
具体的组织方式如下图所示,值得注意的是,队列的第一个节点是哨兵节点,不代表具体的请求锁的节点。
1、writeLock
public long writeLock() {
long s, next; // bypass acquireWrite in fully unlocked case only
return ((((s = state) & ABITS) == 0L &&
U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
next : acquireWrite(false, 0L));
}
ABITS = 128 ,二进制为1111 1111
只有当state的低八位为 0000 0000 的时候才能申请读锁,这个时候按照我们之前说的规则,就是没有写锁,并且读锁的个数为0.
然后利用CAS来申请锁,如果申请成功就将state+=WBIT
WBIT为128
state的低8位就变成 1000 0000,就代表着上了写锁,返回state的值。
如果申请锁失败就进入acquireWrite,自旋申请锁,如果申请不到就挂起
private long acquireWrite(boolean interruptible, long deadline) {
WNode node = null, p;
for (int spins = -1;;) {
long m, s, ns;
//如果当前没有锁,就申请锁
if ((m = (s = state) & ABITS) == 0L) {
if (U.compareAndSwapLong(this, STATE, s, ns = s + WBIT))
return ns;
}
//如果当前是写锁,并且队列亿初始化,但是队列中没有其他节点,就尝试一直自旋获取锁。如果spins = 0.不满足((m = (s = state) & ABITS) == 0L)的话就要执行下面的入队操作了,因为spins > 0 和 spins < 0 的分支都会直接跳过
else if (spins < 0)
spins = (m == WBIT && wtail == whead) ? SPINS : 0;
else if (spins > 0) {
//将自旋次数--,如果自旋次数==-1就会执行else if (spins < 0),再次判断是否自旋
if (LockSupport.nextSecondarySeed() >= 0)
--spins;
}//如果wtail==null,也就是队列为空
else if ((p = wtail) == null) {
//初始化queue
WNode hd = new WNode(WMODE, null);
//将tail==head
if (U.compareAndSwapObject(this, WHEAD, null, hd))
wtail = hd;
}
else if (node == null)//如果node还未初始化,就初始化node,
node = new WNode(WMODE, p);//node为写锁模式,prev=p,p就是tail
else if (node.prev != p)
node.prev = p;//将node的前继节点设为tail
//cas将node的设置为tail
else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
p.next = node;//将原本的tail的next设置现在的tail node
//打断自旋,现在已经入队了
break;
}
}
//已经入队了,开始挂起
for (int spins = -1;;) {
WNode h, np, pp; int ps;
//p是node的前继节点
if ((h = whead) == p) {//如果是队列中的第二个节点,就开始抢锁,第一个节点是哨兵节点
if (spins < 0)
spins = HEAD_SPINS;//1<<10
else if (spins < MAX_HEAD_SPINS)//1<<16
spins <<= 1;
for (int k = spins;;) {
long s, ns;
//开始申请锁
if (((s = state) & ABITS) == 0L) {
if (U.compareAndSwapLong(this, STATE, s,
ns = s + WBIT)) {
//申请锁成功,将node从queue中移除
whead = node;
node.prev = null;
return ns;
}
}
//抢锁失败,k--;
else if (LockSupport.nextSecondarySeed() >= 0 &&
--k <= 0)
break;
}
}
//如果head不为空
else if (h != null) {
WNode c; Thread w;
//如果h为悲观读锁节点,则唤醒所有的悲观读锁节点,不理解的可以看上面WNode的图
while ((c = h.cowait) != null) {
//
if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
(w = c.thread) != null)
//唤醒线程
U.unpark(w);
}
}
//如果其他线程在上述代码执行期间没有释放锁,就尝试将线程挂起啊
if (whead == h) {
//如果node的前继节点不是p
if ((np = node.prev) != p) {
if (np != null)
//将p的next设置为node
(p = np).next = node;
}
else if ((ps = p.status) == 0)
//将p的waitStatus设置为WATING
U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
else if (ps == CANCELLED) {
//如果p被取消了,将p踢出queue,并将p.prev和node连接起来
if ((pp = p.prev) != null) {
node.prev = pp;
pp.next = node;
}
}
else {
long time; //
//deadline == 0说明抢锁没有时间限制
if (deadline == 0L)
time = 0L;
//如果有时间限制,并且超时的话,将node设置为cancel状态
else if ((time = deadline - System.nanoTime()) <= 0L)
return cancelWaiter(node, node, false);
//线程对象
Thread wt = Thread.currentThread();
//将线程加入挂起
U.putObject(wt, PARKBLOCKER, this);
node.thread = wt;
//如果前继节点为WAITING,并且再一次抢锁失败就挂起
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);
//如果被中断了,就将node设置为cancel
if (interruptible && Thread.interrupted())
return cancelWaiter(node, node, true);
}
}
}
}
抢写锁步骤:
1、先请求一次抢锁,抢不到转2
2、如果当前队列为空,就开始自旋抢锁,如果队列不为空就挂起
3、如果当前队列不为空了,并且在自旋期间没抢到锁,就开始挂起
4、将当前线程包装成WNode节点,并加入到queue的末尾
5、如果当前线程处于queue中的第二个节点,head是个哨兵节点,说明就很有可能抢到锁,就会开始新一轮的自旋抢锁。
6、如果不是在queue中的第二个节点,就将prev节点的waitstatus设置为WAITING(-1),并将waitstatus=CANCEL(1)的节点从queue中去除
5、挂起,但其他线程释放锁的时候,如果将当前线程唤醒就转到步骤5继续执行
unlockWrite
public void unlockWrite(long stamp) {
WNode h; 1000 0000 1000 0000
//如果state != stamp,并且 stamp & WABIT == 0 说明被Stamp的写锁标志变为0了,这就属于异常了
if (state != stamp || (stamp & WBIT) == 0L)
throw new IllegalMonitorStateException();
// state = stamp + WBIT 会把写锁标志的1变为0,并向前进1,也就是版本号会加1
state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
//队列不为空,唤醒后继节点
if ((h = whead) != null && h.status != 0)
release(h);
}
private void release(WNode h) {
if (h != null) {//队列不为空
WNode q; Thread w;
//将h的waitstatus变为0,
U.compareAndSwapInt(h, WSTATUS, WAITING, 0);
//1、h的next节点被取消了
//2、h的next节点为null,并不代表队列为空,因为是在入队的时候先设置的node.prev = p,最后才设置的p.next = node,所以要从尾到头遍历
if ((q = h.next) == null || q.status == CANCELLED) {
for (WNode t = wtail; t != null && t != h; t = t.prev)
//如果t为WAITING
if (t.status <= 0)
q = t;
}
if (q != null && (w = q.thread) != null)
//唤醒线程,去抢锁锁
//之前挂起线程是在一个死循环中挂起的,直到超时或者申请成功锁后,才会退出
U.unpark(w);
}
}
释放写锁步骤
1、首先根据抢锁的时候返回的stamp来检验是否发生异常
2、如果没发生异常,就通过state+=WBIT将写锁的标志置为0,并将版本号+1
3、唤醒后继的第一个节点来抢锁
readLock()
public long readLock() {
long s = state, next;
//1、队列为空
//2、s & 255 < 126 如果 (s & ABITS) >= RFULL 说明当前为写模式,或者超出了读锁的最大线程个数
// 1111 1111
// &
// 1000 0000 = 1000 0000 = 128 写锁模式
// 1111 1111
// 0111 1111 = 111 111 = 127 读模式 最多容纳 127个线程
//3、state = s + runit(1),也就是悲观读线程数+1
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;
//如果whead == wtail,队列为空
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))
//成功返回state
return ns;
//如果现在是写锁模式 m = state & ABITS
else if (m >= WBIT) {
if (spins > 0) {
//自旋次数-1
if (LockSupport.nextSecondarySeed() >= 0)
--spins;
}
else {
//如果自旋次数用完,
if (spins == 0) {
//h为自旋前的whead,p为自旋前的wtail
WNode nh = whead, np = wtail;
//如果nh = h && np = p,说明在自旋期间根本没有释放锁,或者队列不为空,就挂起线程
if ((nh == h && np == p) || (h = nh) != (p = np))
break;
}
//spin = 64
spins = SPINS;
}
}
}
}//第一阶段自旋代码到此结束
//开始入队
//如果tail = null,开始初始化queue,head只是一个哨兵节点
if (p == null) { //初始化
WNode hd = new WNode(WMODE, null);
if (U.compareAndSwapObject(this, WHEAD, null, hd))
wtail = hd;
}
else if (node == null)//初始化node,p是node的prev前继节点
node = new WNode(RMODE, p);
//如果队列为空,但是被初始化了,或者wtail节点对应的是写锁模式
else if (h == p || p.mode != RMODE) {
//设置node.prev = tail
if (node.prev != p)
node.prev = p;
//将node插入到queue中
else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
p.next = node;
//停止自旋,开始挂起
break;
}
}
//p是node的前继节点,如果p的mode是读锁,就将node通过cowait指针来链接到p节点中。
else if (!U.compareAndSwapObject(p, WCOWAIT,
node.cowait = p.cowait, node))
node.cowait = null;
else {
//插入前尾节点是读锁的情况,通过cowait加入,这时whead不是哨兵节点,而是读锁。当第一个读锁对象争夺锁的时候,会将whead=node的。
for (;;) {
WNode pp, c; Thread w;
//如果whead为读锁的时候,将其cowait唤醒来抢锁,被唤醒的节点然后唤醒其cowait,速度很快。
if ((h = whead) != null && (c = h.cowait) != null &&
U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
(w = c.thread) != null)
U.unpark(w);
//抢锁,当m<WBIT,此时无写锁
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;
if (pp == null || h == p || p.status > 0) {
node = null; // throw away
break;//
}
if (deadline == 0L)
time = 0L;
//超时了,将node取消
else if ((time = deadline - System.nanoTime()) <= 0L)
return cancelWaiter(node, p, false);
//后面就是将线程挂起
Thread wt = Thread.currentThread();
U.putObject(wt, PARKBLOCKER, this);
node.thread = wt;
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);
}
}
}
}
//插入前的尾节点是写锁
for (int spins = -1;;) {
WNode h, np, pp; int ps;
//如果node处于第二个节点,可以开始抢锁了,开始自旋吧
if ((h = whead) == p) {
if (spins < 0)//设置自旋次数
spins = HEAD_SPINS;//1<<10
else if (spins < MAX_HEAD_SPINS)//1<<16
spins <<= 1;
for (int k = spins;;) { // spin at head
long m, s, ns;
//开始抢锁,
//如果m < 126 就直接将state+=1
//如果m == 126,就会调用tryIncReaderOverflow(),代码如下
//if ((s & ABITS) == RFULL) {
// s 后8为 126 ,RBITS=127,s|RBITS的后八位 为 1,然后将readerOverflow++,state 的后八位 为 1. 出现异常就返回0
// if (U.compareAndSwapLong(this, STATE, s, s | RBITS)) {
// ++readerOverflow;
// state = s;
// return s;
// }
// }
if ((m = (s = state) & ABITS) < RFULL ?
U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
//如果m == RFULL(126),调用tryIncReaderOverflow()
(m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L)) {//抢锁成功了
WNode c; Thread w;
//将whead设置为node
whead = node;
//断了node和whead的联系
node.prev = null;
//通过cowait依次将node的cowait唤醒
while ((c = node.cowait) != null) {
if (U.compareAndSwapObject(node, WCOWAIT,
c, c.cowait) &&
(w = c.thread) != null)
U.unpark(w);
}
return ns;
}
//如果被写锁抢占了,就break,将线程挂起
else if (m >= WBIT &&
LockSupport.nextSecondarySeed() >= 0 && --k <= 0)
break;
}
}
else if (h != null) {//如果将node设置为whead了
WNode c; Thread w;
while ((c = h.cowait) != null) {//唤醒node的cowait去抢锁
if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
(w = c.thread) != null)
U.unpark(w);
}
}
if (whead == h) {//如果没有将node设置为whead,就该挂起线程了
//为node找到合适的p,并将p设置为WAITING
if ((np = node.prev) != p) {
if (np != null)
(p = np).next = node;
}
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;
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);
}
}
}
}
读锁抢锁步骤
1、首先判断whead == wtail && (s & ABITS) < RFULL &&
U.compareAndSwapLong(this, STATE, s, next = s + RUNIT),这时就算申请成功
2、如果申请不成功,就尝试自旋申请锁,如果whead == wtail,就会自旋申请锁,如果whead!=wtail就退出自旋。
3、如果申请不成功,就将线程包装为WNode的node,然后这时分两种情况。
4、情况1:当前尾节点是写锁,就将node通过next和prev与尾节点联系起来,将node设置为新的尾节点。这个情况下,如果node满足是队列中的第二个节点,就开始自旋申请锁。申请成功后会将node设置为whead。
如果不满足队列中第二个节点的条件就将线程挂起
5、情况2:如果当前尾节点是读锁,就将node通过cowait指针与尾节点联系起来。如果这时h.cowait不为空,说明情况4已经获取到锁了,whead是一个读锁。如果h.cowait不为空,就会将其对应的线程唤醒来抢锁。唤醒的线程会继续将其cowait唤醒来抢锁。
如果h.cowait为空,说明情况4的锁还未申请成功,这个时候就会将线程挂起。
读锁的释放
public void unlockRead(long stamp) {
long s, m; WNode h;
for (;;) {
//将stamp与state检验,如果不符合就说明出现异常了,抛出异常。
if (((s = state) & SBITS) != (stamp & SBITS) ||
(stamp & ABITS) == 0L || (m = s & ABITS) == 0L || m == WBIT)
throw new IllegalMonitorStateException();
if (m < RFULL) {
if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) {
if (m == RUNIT && (h = whead) != null && h.status != 0)
release(h);
break;
}
}
// m = 126 ,ABITS = 255 ,则state = 126
//如果readerOverflow != 0,并且m = RFULL,将state设置为126
else if (tryDecReaderOverflow(s) != 0L)
break;
}
}
private long tryDecReaderOverflow(long s) {
//如果ABITS = 255 ,s的后八位为 126 ,ABITS & s = RFULL
if ((s & ABITS) == RFULL) {
if (U.compareAndSwapLong(this, STATE, s, s | RBITS)) {
int r; long next;
//如果readerOverflow > 0, 将state = s|RBITS(127) ,state的后8位为1
if ((r = readerOverflow) > 0) {
readerOverflow = r - 1;
next = s;
}
else
next = s - RUNIT;//state-1;
state = next;
return next;
}
}
else if ((LockSupport.nextSecondarySeed() &
OVERFLOW_YIELD_RATE) == 0)
Thread.yield();
return 0L;
}
tryOptimisticRead
StampedLock支持乐观读,乐观读并不会加锁,所以会提高读的效率。
public long tryOptimisticRead() {
long s;
//state&WBIT == 0,说明当前不存在写锁,返回 s& SBITS
//WBIT = 128 SBITS = ~RBITS(127),SBITS的后7位为0,其他都为1
//s & SBITS 就是我们前25位,版本号+锁标志(0)。
//state&WBIT != 0,说明当前存在写锁,返回0
return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}
//验证
public boolean validate(long stamp) {
U.loadFence();//使用LoadLoad屏障,禁止将之前的读取成员属性的操作重排序到后面的验证逻辑之后
//将之前返回的stamp和当前state对比
//SBITS = ~RBITS(127)
//SBITS的后7位为0,其他都为1
//一、stamp = 0,有写锁。 stamp & SBITS = 0,
state & SBITS
1、当前有写锁,锁标志位肯定为1 ,那么 state & SBITS肯定不等0,最起码锁标志对应的那一位就肯定为1,state & SBITS != stamp & SBITS
2、当前无锁,之前的写锁释放,版本号肯定+1,不为0。state & SBITS肯定也不等0,state & SBITS != stamp & SBITS
//二、stamp,无写锁, stamp & SBITS就是版本号+锁标志0。
1、当前有锁,锁标志位肯定为1 ,那么 state & SBITS的最后一位锁标志是1,不相等
2、当前无锁,如果不存在写锁的申请和是释放,版本号不变,state & SBITS == stamp & SBITS ,版本号一样,锁标志还是0
如果存在写锁的申请和释放,版本号会变, state & SBITS != stamp & SBITS,虽然锁标志还是0,但是版本号不一样。
综上所述,当在乐观读中间发生写操作的时候,会返回false
return (stamp & SBITS) == (state & SBITS);
}
总结
StampedLock也是基于state和队列实现的,只不过不能重入。通过引入了乐观读并不加锁,在读多写少的场景中性能会比ReentrantReadWriteLock要好!
StampedLock和ReentrantReadWriteLock的队列也有区别:
1、StampedLock中的连续申请的读锁节点通过cowaiter新起了一个维度链表来存放。而ReentrantReadWriteLock的读锁节点是一个一个的排列在队列中的。