StampedLock的小陷阱
StampedLock实现时,使用类似于CAS操作的死循环反复尝试的策略。在它挂起线程时,使用的是Unsafe.park()方法,而park()方法在遇到线程中断时,会直接返回(注意,不同于Thread.sleep()方法,它不会抛出异常)。在StampedLock的死循环逻辑中,没有处理中断的逻辑,这就会导致阻塞在park()方法上的线程被中断后,再次进入循环。而当退出条件得不到满足时,就会发生疯狂占用CPU的情况。
前言
StampedLock的写锁是不可重入锁。使用的数据结构不是AQS,是类似于AQS的队列,在自旋加入队列时也会去抢锁。维护了一个读线程的链表,唤醒一个则唤醒全部。StampedLock不响应中断。当被中断信号唤醒时会不断自旋占用CPU。
一、为什么引入StampedLock
ReentrantLock采用的是“悲观读”的策略,当第一个读线程拿到锁之后,第二个、第三个读线程还可以拿到锁,使得写线程一直拿不到锁,可能导致写线程“饿死”。StampedLock引入了“乐观读”策略,读的时候不加读锁,读出来发现数据被修改了,再升级为“悲观读”,相当于降低了“读”的地位,避免写线程被饿死。
二、“乐观读”的实现原理
// 用于计算state值的位常量
private static final long RUNIT = 1L;
private static final long WBIT = 1L << LG_READERS; //第八位表示写锁
private static final long RBITS = WBIT - 1L; //最低的7位表示读锁
private static final long RFULL = RBITS - 1L; //读锁的数量
private static final long ABITS = RBITS | WBIT; //读锁和写锁的状态合在一起
private static final long SBITS = ~RBITS;
// 初始化state值
private static final long ORIGIN = WBIT << 1;
// 同步状态state,第八位为写锁,前七位为读锁
private transient volatile long state;
// 读锁只占七位,最大值为128,使用readerOverflow来记录溢出的读锁
private transient int readerOverflow;
public long tryOptimisticRead() {
long s; // 没有线程获取写锁,则乐观读锁获取成功,返回票据
return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}
public boolean validate(long stamp) {
//插入内存屏障,使得用户数据同步
U.loadFence();
// 传入乐观读锁stamp,验证是否有线程获取到写锁
return (stamp & SBITS) == (state & SBITS);
}
三、悲观读/写:“阻塞”和“自旋”策略实现差异
和ReadWriteLock一样,StampedLock也要进行悲观的读锁和写锁操作。不过它不是基于AQS实现的,而是内部重新实现了一个阻塞队列。
这个阻塞队列和AQS很像,刚开始的时候whead=wtail=NULL,然后初始化,建立一个空节点。
但是“自旋”策略不一样,AQS中CAS失败后直接加入阻塞队列中。而在StampedLock中,CAS失败后会不断自旋,自旋足够多的次数后,如果还拿不到锁才进入阻塞队列。 如果是单核的CPU,肯定不能自旋,在多核CPU下,才采用自旋策略。
private static final int SPINS = (NCPU > 1) ? 1 << 6 : 0;
public long writeLock() {
long s, next;
return ((((s = state) & ABITS) == 0L &&
U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
next : acquireWrite(false, 0L));
}
当state&ABITS == 0的时候,说明既没有读锁也没有写锁,此时当前线程才有资格通过CAS操作state。如果CAS失败,调用acquireWrite进入阻塞队列并进行自旋,这个函数是加锁的核心。
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代表没有写线程在阻塞,(s & ABITS) < RFULL表示当前持有的是读锁且数量没有到极限。
acquireWrite函数会不断CAS,一边尝试获得锁,一边尝试加入队列。如果加入了队列,循环一定一定次数,当发现自己处于队列头部时再进行CAS抢锁,一定次数下没获得锁进入阻塞。当release函数被调用时,头部节点被唤醒继续后续逻辑。
另外一个不同于AQS队列的是,每个WNode里面有一个cowait指针,用于串联其所有的读线程,当其中一个线程被唤醒,其他线程一起被唤醒。
// java.util.concurrent.locks.StampedLock#unlockWrite
public void unlockWrite(long stamp) {
WNode h;
if (state != stamp || (stamp & WBIT) == 0L)
throw new IllegalMonitorStateException();
state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
if ((h = whead) != null && h.status != 0)
release(h);
}
// 唤醒队列的队首节点【头结点whead的后继节点】
private void release(WNode h) {
if (h != null) {
WNode q; Thread w;
U.compareAndSwapInt(h, WSTATUS, WAITING, 0); // 将头结点状态从-1变为0,标识要唤醒其后继节点
if ((q = h.next) == null || q.status == CANCELLED) { // 判断头结点的后继节点是否为null或状态为取消
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); // 唤醒队首节点
}
}
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)
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;
}
}
else if (tryDecReaderOverflow(s) != 0L)
break;
}
}