并发包源码解读——StampedLock
我们还是以类似这样的简单例子开头
public static void main(String[] args) throws InterruptedException {
final StampedLock stampedLock = new StampedLock();
final long stamp = stampedLock.writeLock();
Thread.sleep(1000);
new Thread(() ->{
stampedLock.writeLock();
}).start();
stampedLock.unlockRead(stamp);
}
1、构造器
state表示锁的状态,ORIGIN是一个初始值,同样还有好多和他们类似的初始值,可以看到他们都是经过了一系列位运算算出来的,可以算一下ORIGIN的值是256,为了方便观察我们记为1,00000000
public StampedLock() {
state = ORIGIN;
}
private static final int LG_READERS = 7;
//long是8字节也就是64位,前面若干0都省略
//00000001
private static final long RUNIT = 1L;
//10000000
private static final long WBIT = 1L << LG_READERS;
//01111111
private static final long RBITS = WBIT - 1L;
//01111110
private static final long RFULL = RBITS - 1L;
//11111111
private static final long ABITS = RBITS | WBIT;
//高57位为1,低7位为0
private static final long SBITS = ~RBITS;
//1,00000000
private static final long ORIGIN = WBIT << 1;
2、写锁
state低8位表示获取锁的状态,其中第8位表示是否加了写锁,低7位表示加读锁的次数
当我们取state低8位结果为0时,说明没人持有锁,那就放心地用cas改变持锁状态,state值为384也就是110000000
public long writeLock() {
long s, next;
return ((((s = state) & ABITS) == 0L &&
U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
next : acquireWrite(false, 0L));
}
像STATE这种常量,是在调用构造器的时候初始化的
// Unsafe mechanics
private static final sun.misc.Unsafe U;
private static final long STATE;
private static final long WHEAD;
private static final long WTAIL;
private static final long WNEXT;
private static final long WSTATUS;
private static final long WCOWAIT;
private static final long PARKBLOCKER;
static {
try {
U = sun.misc.Unsafe.getUnsafe();
Class<?> k = StampedLock.class;
Class<?> wk = WNode.class;
//见名知意,这个objectFieldOffset是根据对象属性的偏移量取值,前三个是属于锁的,中间3个是属于当前节点的
STATE = U.objectFieldOffset
(k.getDeclaredField("state"));
WHEAD = U.objectFieldOffset
(k.getDeclaredField("whead"));
WTAIL = U.objectFieldOffset
(k.getDeclaredField("wtail"));
WSTATUS = U.objectFieldOffset
(wk.getDeclaredField("status"));
WNEXT = U.objectFieldOffset
(wk.getDeclaredField("next"));
WCOWAIT = U.objectFieldOffset
(wk.getDeclaredField("cowait"));
//这个是专门记阻塞节点对应的线程的,一一对应
Class<?> tk = Thread.class;
PARKBLOCKER = U.objectFieldOffset
(tk.getDeclaredField("parkBlocker"));
} catch (Exception e) {
throw new Error(e);
}
}
如果cas持锁失败,那就进入acquireWrite方法
private long acquireWrite(boolean interruptible, long deadline) {
WNode node = null, p;
for (int spins = -1;;) { // spin while enqueuing
long m, s, ns;
//前三个if、else if、else if是明显的自旋锁,最大自旋次数为SPINS的值64次,但实际上可能会比64次多,
//因为LockSupport.nextSecondarySeed()生成随机数>=0才减
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;
//第一波自旋时尾节点已经赋值给了p,其实p就是当前节点
//如果头节点就是自己,就再自旋获取HEAD_SPINS也就是1024次,取锁失败了就扩大一倍尝试次数再试,最大能到MAX_HEAD_SPINS也就是65535次,除非线程抢到锁return,否则无法跳出自旋(其实就是线程阻塞了不断尝试获取锁)
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;
}
}
//说明本轮cas次数已达上限,跳出内自旋进行后续逻辑
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
}
//如果尾节点状态是0就让他它等待
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;
//该节点处于等待状态、或者队列中有线程获取锁,并且队列首尾都没变,就调用park阻塞0秒(其实就是不阻塞)
if (p.status < 0 && (p != h || (state & ABITS) != 0L) &&
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);
}
}
}
}
3、读锁
接下来我们试试readLock
加读锁调用readLock,如果有线程加读锁到上限,就成功获取锁
public long readLock() {
long s = state, next; // bypass acquireRead on common uncontended case
return ((whead == wtail && (s & ABITS) < RFULL &&
U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
next : acquireRead(false, 0L));
}
失败就进入acquireRead尝试获取读锁
其实套路跟写锁一样,都是先尝试自旋尝试获取锁
private long acquireRead(boolean interruptible, long deadline) {
WNode node = null, p;
for (int spins = -1;;) {
WNode h;
//队列中首尾节点相同,说明前面就一个节点,快轮到自己了,就自旋用cas获取锁
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) {
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;
}
spins = SPINS;
}
}
}
}
//尾节点是空,就初始化一个写的头节点,因为能阻塞获取读锁的就只有写锁(也就是初始化一个独占模式的头节点)
//首尾节点初始化为同一个节点
if (p == null) { // initialize queue
WNode hd = new WNode(WMODE, null);
if (U.compareAndSwapObject(this, WHEAD, null, hd))
wtail = hd;
}
//当前节点node为空,就初始化一个当前节点,把当前节点的前驱指向尾节点,作为入队后新的尾节点
else if (node == null)
node = new WNode(RMODE, p);
//如果首尾节点相同,或者尾节点加的写锁(或者说不是共享模式的节点,是独占模式的节点)
else if (h == p || p.mode != RMODE) {
//当前节点的前驱不是尾节点就重置一下
if (node.prev != p)
node.prev = p;
//当前节点的前驱是尾节点,就用cas把尾节点更新为当前节点
else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
p.next = node;
//成功进队之后,第一次自旋就结束了
break;
}
}
//如果尾节点加的读锁(是共享模式的节点),就把尾节点的cowait更新为当前节点,失败的话就重置
//(我们可以发现,每个节点都有cowait,而cowait也是节点,所以这个cowait是一条链,里面是读节点相连,而这条‘读链’是在‘读链的头节点’作为阻塞队列的尾节点时被添加进去的)
else if (!U.compareAndSwapObject(p, WCOWAIT,
node.cowait = p.cowait, node))
//cas失败了,说明有别的读锁先于当前线程更新了尾节点的cowait,那当前线程的cowait就重置
node.cowait = null;
//如果cas更新当前节点的cowait成功了
else {
for (;;) {
WNode pp, c; Thread w;
//如果队列中有头节点,且头节点有cowait,就移除头节点下的读链表中第一个读节点然后唤醒它
//并且唤醒头节点的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);
//如果首尾节点挨着或者相同,或者尾节点前驱是空的
if (h == (pp = p.prev) || h == p || pp == null) {
long m, s, ns;
//总的一句话就是自旋cas获取读锁
do {
//如果现在没写锁了,或者加读锁次数没到上限,就cas获取读锁
//如果加读锁次数也到上限了,就尝试获取读锁并记一次溢出
//获取成功了就return
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;
//代码执行到这已经没人占着锁了,就break出去
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;
if ((h != pp || (state & ABITS) == WBIT) &&
whead == h && p.prev == pp)
U.park(false, time);
node.thread = null;
U.putObject(wt, PARKBLOCKER, null);
//这里基本上就不会进来,因为interruptible显式为false
if (interruptible && Thread.interrupted())
return cancelWaiter(node, p, true);
}
}
}
}
//第二波自旋
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 m, s, ns;
//加读锁次数没到上限,就cas改状态获取读锁,成功了就把自己变成头节点并移除前驱
//cas失败了就尝试获取读锁然后记读溢出
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;
//如果当前节点有cowait,就依次唤醒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) {
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) {
if ((np = node.prev) != p) {
//如果当前节点有前驱却不是尾节点,就把前驱的后继指向自己,这也算是一步复位
if (np != null)
(p = np).next = node; // stale
}
//如果尾节点状态是0,那就更新为等待
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);
//这里基本上就不会进来,因为interruptible显式为false
if (interruptible && Thread.interrupted())
return cancelWaiter(node, node, true);
}
}
}
}
自旋中有个tryIncReaderOverflow方法,为了在加读锁达到上限之时,让之前已经进入自旋或阻塞的读锁后续能获取锁
private long tryIncReaderOverflow(long s) {
// assert (s & ABITS) >= RFULL;
//如果加读锁的次数达到上限,就重置回RBITS,然后记一次读溢出
if ((s & ABITS) == RFULL) {
if (U.compareAndSwapLong(this, STATE, s, s | RBITS)) {
++readerOverflow;
state = s;
return s;
}
}
//如果随机到了true,就给其他线程执行
else if ((LockSupport.nextSecondarySeed() &
OVERFLOW_YIELD_RATE) == 0)
Thread.yield();
//没到上限就返回0
return 0L;
}
当获取锁超时了,调用cancelWaiter
private long cancelWaiter(WNode node, WNode group, boolean interrupted) {
//group是传进来的尾节点
if (node != null && group != null) {
Thread w;
node.status = CANCELLED;
//循环移除尾节点的读链表中取消状态的节点
for (WNode p = group, q; (q = p.cowait) != null;) {
if (q.status == CANCELLED) {
U.compareAndSwapObject(p, WCOWAIT, q, q.cowait);
p = group; // restart
}
//否则就在下一次循环时使用当前节点的cowait判断状态,直到最后节点没有cowait
else
p = q;
}
//如果最后一个节点下的读链表中,第一个节点就是当前节点
if (group == node) {
//唤醒尾节点读链表中所有的节点对应的线程,让他们重新去竞争排队
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; // find valid successor
//如果当前节点没后继或者后继是取消状态
while ((succ = node.next) == null ||
succ.status == CANCELLED) {
WNode q = null; // find successor the slow way
//从队尾循环到当前待取消的节点
for (WNode t = wtail; t != null && t != node; t = t.prev)
if (t.status != CANCELLED)
q = t; // don't link if succ cancelled
//如果循环到当前节点的后继了,或者这次循环的节点cas放到当前节点的后继了
if (succ == q || // ensure accurate successor
U.compareAndSwapObject(node, WNEXT,
succ, succ = q)) {
//如果当前节点的后继为空,并且当前节点就是尾节点,就cas把尾节点改成当前节点的前驱
//这个时候当前节点才与阻塞队列的联系断开
if (succ == null && node == wtail)
U.compareAndSwapObject(this, WTAIL, node, pred);
break;
}
}
//如果没有新节点插进来的话,就把它的前驱指向它(之前是它指向它的前驱,单向关联,现在就是双向关联)
if (pred.next == node) // unsplice pred link
U.compareAndSwapObject(pred, WNEXT, node, succ);
//这个节点取消了,就唤醒下一个节点
if (succ != null && (w = succ.thread) != null) {
succ.thread = null;
U.unpark(w); // wake up succ to observe new pred
}
//如果当前节点的前驱的状态不是取消,或者前驱的前驱为空就跳出循环
if (pred.status != CANCELLED || (pp = pred.prev) == null)
break;
node.prev = pp; // repeat if new pred wrong/cancelled
//succ是while循环中当前待取消节点的后继,pred是for循环中当前节点的前驱
//那作用就很明显了,把当前节点的前驱和后继连起来,把自己替出去
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) {
//如果头节点的后继是空,或者后继是取消状态,就从后往前找状态<0的第一个节点
for (WNode t = wtail; t != null && t != h; t = t.prev)
if (t.status <= 0)
q = t;
}
if (h == whead) {
//之前在cas获取锁的时候如果如果失败了,会放到尾节点并标记status为等待状态
//当头节点的状态是0了说明轮到头节点获取锁了,那就得移除头节点
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;
}
最后一步是调用release
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);
}
}
4、乐观读锁
本人不才写了一段丑陋易懂的代码
首先说一下怎么用
1、调用tryOptimisticRead方法返回一个戳stamp
2、本地赋值(读出来)
3、调用validate方法校验stamp,返回在读期间写锁版本号是否改变
4、改变的话升级为悲观读,重新赋值,然后释放悲观读锁
package com.company;
import java.util.concurrent.locks.StampedLock;
public class Main {
private static int i;
public static void main(String[] args) {
final StampedLock stampedLock = new StampedLock();
new Thread(new ReadThread(stampedLock)).start();
new Thread(new WriteThread(stampedLock)).start();
}
static class ReadThread implements Runnable{
private final StampedLock stampedLock;
public ReadThread(StampedLock stampedLock) {
this.stampedLock = stampedLock;
}
@Override
public void run() {
long stamp = stampedLock.tryOptimisticRead();
int icopy = i;
System.out.println("第一次读"+icopy);
if(!stampedLock.validate(stamp)){
System.out.println("有写锁把值改了");
stamp = stampedLock.readLock();
icopy = i;
stampedLock.unlockRead(stamp);
}
System.out.println("最后的读"+icopy);
}
}
static class WriteThread implements Runnable{
private final StampedLock stampedLock;
public WriteThread(StampedLock stampedLock) {
this.stampedLock = stampedLock;
}
@Override
public void run() {
long stamp = stampedLock.writeLock();
System.out.println("加写锁");
i++;
System.out.println(i);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("释放写锁");
stampedLock.unlockWrite(stamp);
}
}
}
接下来再讲原理
调用tryOptimisticRead方法,取状态的高57位作为stamp返回
public long tryOptimisticRead() {
long s;
return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}
可以发现乐观读只使用了位运算,并没有真正的加锁。刚才我们说了,高57位表示读锁的版本,只要我们在加乐观读锁之后判断写锁的版本号有没有改变(在读期间有没有加过写锁,数据有没有改变),就可以保证数据的可靠性
校验的方法是validate
首先调用了unsafe的loadFence方法,见名知意我们翻译为读屏障,用来保证loadFence之前的所有读操作都完成
这个读操作是指把主存中的数据都刷进线程缓存,保证内存一致性
public boolean validate(long stamp) {
U.loadFence();
return (stamp & SBITS) == (state & SBITS);
}
实际上我还是没太懂为什么要调用loadFence,猜测是为了把stamp刷进缓存
5、释放锁
直接调用unlock方法,传入当前线程加锁时生成的stamp
public void unlock(long stamp) {
long a = stamp & ABITS, m, s; WNode h;
while (((s = state) & SBITS) == (stamp & SBITS)) {
//都已经没锁了就直接break
if ((m = s & ABITS) == 0L)
break;
//如果加的写锁(m刚才state给赋值了,是加锁状态)
else if (m == WBIT) {
//释放锁用的标志对不上(跟加锁返回的stamp不同),就break抛异常
if (a != m)
break;
//这里不用cas减WBIT反而要加,这样不仅不会影响写位的判断,还能通过每次写操作给锁赋予‘版本更新’的特性
//但版本也是有上限的,long一共64位,低7位表示读次数,高57位区分写版本号,溢出之后重置为ORIGIN
state = (s += WBIT) == 0L ? ORIGIN : s;
//等待队列中有头节点且下一个节点状态不是初始状态,就移除头节点
if ((h = whead) != null && h.status != 0)
release(h);
return;
}
//没加读锁,状态不对就break抛异常(释放锁的时候不可能没锁或者读写锁同时存在)
else if (a == 0L || a >= WBIT)
break;
//如果加读锁次数小于加读锁的次数上限,这种状态是正常的
else if (m < RFULL) {
//cas尝试释放一个读锁
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();
}