StampedLock详解

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;

因为写锁只有一个bit位,所以写锁时不可重入的。

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;
    }
}

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值