2

StampedLock

最近看并发工具类的遇到这个玩意,据说效率比ReentrantLock高,咱也没用过,闲得慌,源码先扒了再说,不定期更新。

锁状态变量

既然是类似读写锁,肯定也有变量用于记录锁状态,相对于读写锁使用Int,StampedLock中使Long类型(64位)表示锁状态,其中第8位表示写锁持有状态,低7位用于表示持有读锁的线程数。(目前还未搞懂为啥用Long,待后面研究)

    private static final long RUNIT = 1L; //读锁线程增加时,
    //获取写锁状态的掩码:1000 000
    private static final long WBIT  = 1L << LG_READERS; 
    //获取读锁状态的掩码:0111 1111
    private static final long RBITS = WBIT - 1L;
    //读锁持有数量的上限
    private static final long RFULL = RBITS - 1L;
    //获取锁的状态,用于判断是否有线程持有读锁或写锁
    private static final long ABITS = RBITS | WBIT;
    //RBITS 取反
    private static final long SBITS = ~RBITS; // note overlap with ABITS

写锁获取

    public long writeLock() {
        long s, next; 
        //如果没有任何线程持有锁,CAS尝试添加写锁状态
        //如果失败,则acquireWrite方法中自旋尝试获锁,再次失败入队
        return ((((s = state) & ABITS) == 0L &&
                 U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
                next : acquireWrite(false, 0L));
    }

acquireWrite(方法巨长,真难看),该方法其实就是自旋获取锁,获取不到要么协助唤醒,要不就挂起,过程如下:
1、刚开始,啥都不管,我就要去拿写锁,不停地自旋
2、自旋完了还是搞不赢,只能先入个队(尾插)观望一波,注意,只是入个队,没挂起线程
3、那入队后观望啥呢?肯定是看看自己排在哪里呀,如果是排在队列头结点的后面,那机会又来了,开始不要命地自旋去获锁。(因为头结点是一个拿到锁的结点,后继结点作为二把手肯定要开始抢锁了)
4、如果发现自己排在老后面无希望拿到写锁怎么办?躺平也不是事,得想办法加快自己获取锁的进程;如果头结点是获取读锁线程的结点,如下图中ReaderNode1那种,是个栈,那就要帮忙唤醒整队列中挂起的线程,以提高自己获取锁的速度。
image.png
5、那如果条件还是不满足怎么办?那就只能挂起当前线程,老老实实等待了(注意会响应超时和中断)
源码分析如下:

 private long acquireWrite(boolean interruptible, long deadline) {
        WNode node = null, p;
        //这个自旋用来尝试获取写锁,自旋结束后,尾插法入CLH队列
        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)
                 //写锁被其它线程占有,且等待队列为空(头等于尾)时,设置自旋次数
                 //多核2的6次方,单核为0
                spins = (m == WBIT && wtail == whead) ? SPINS : 0;
            else if (spins > 0) {
                // 通过随机数来递减自旋次数
                if (LockSupport.nextSecondarySeed() >= 0)
                    --spins;
            }
            //当自旋次数为0的时候,开始初始化等待队列:CLH队列
            //队列初始状态:头结点和尾结点指向同一个结点
            else if ((p = wtail) == null) {
                WNode hd = new WNode(WMODE, null);
                if (U.compareAndSwapObject(this, WHEAD, null, hd))
                    wtail = hd;
            }
            //下面三个if就是将node采用尾插法插入等待队列,并退出自旋
            //注意,此node.thread仍为null,线程还未挂起
            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;;) {
            // h头结点,np,当前结点前置结点,pp当前结点的前置结点的前置结点,ps前置结点状态:0初始状态,-1等待状态,1取消状态
            WNode h, np, pp; int ps;
            //当前node的前置结点就是头结点,那么当前结点可以去尝试去获取锁了(头结点是拿到了锁的)
            //类似AQSd的同步结点:头结点为持锁结点,每次线程释放锁会唤醒头结点的后继结点
            if ((h = whead) == p) {
                if (spins < 0)
                    spins = HEAD_SPINS; // 初始化自旋次数为2的10次方(CPU多核)
                // 当上次自旋spins 次后,仍未获取写锁,将自旋次数增大2倍,继续自旋
                else if (spins < MAX_HEAD_SPINS)
                    spins <<= 1;
                //作为头结点的后继结点,会一直自旋获取写锁,没必要挂起
                for (int k = spins;;) { // spin at head
                    long s, ns;
                    //只要现在锁状态为0,那我就尝试给它添加写锁状态
                    if (((s = state) & ABITS) == 0L) {
                        if (U.compareAndSwapLong(this, STATE, s,
                                                 ns = s + WBIT)) {
                            //获取写锁成功,当前node成为新的头结点                 
                            whead = node;
                            node.prev = null;
                            return ns;
                        }
                    }
                    // 当自旋计数为0时,退出当前循环,spins 扩大2倍再次自旋
                    else if (LockSupport.nextSecondarySeed() >= 0 &&
                             --k <= 0)
                        break;
                }
            }
            // 如果当前队列已经初始化,h!=null,且当前结点不是头结点后继结点
            // 那么当前结点不能获取锁,闲着也是闲着,就帮忙唤醒挂起等待的结点
            else if (h != null) { 
                WNode c; Thread w;
                //如果头结点是读结点cowait栈(CLH队列中,前面是写结点,后面如果是连续的的读结点,不会在CLH中往后排,而是第一读结点为头,拉起一个链表,有点类似HashMap解决Hash冲突时那种结构)
                while ((c = h.cowait) != null) {
                    //cas,是c指向下一个读结点,如果该结点线程不为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
                }
                //当前线程Node的前置结点状态如果为0,设置为等待状态(前面提过0初始状态,-1等待状态,1取消状态)
                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();
                    // jdk bug,解决jstack打印的时候,不显示当前线程挂在哪个对象锁上
                    U.putObject(wt, PARKBLOCKER, this);
                    node.thread = wt;
                    // 当前结点的前驱结点为等待状态
                    //前驱结点不是头结点,或锁状态不为0
                    //或者队列暂无变化
                    if (p.status < 0 && (p != h || (state & ABITS) != 0L) &&
                        whead == h && node.prev == p)
                        //挂起线程
                        U.park(false, time);  // emulate LockSupport.park
                    // 结束挂起后,清理掉node中持有的当前线程引用
                    node.thread = null;
                    U.putObject(wt, PARKBLOCKER, null);
                    //如果线程被唤醒是因为中断,则取消等待
                    if (interruptible && Thread.interrupted())
                        return cancelWaiter(node, node, true);
                }
            }
        }
    }
从上面方法可以看出,不到万不得已,是不会选择挂起线程去等待被唤醒,而是不断尝试通过CAS去拿锁,这也是高性能的由来吧,不过,感觉这种大量的自旋有的时候也是对CPU资源浪费。

当前线程获锁失败时需要等,但支持超时取消和响应中断。cancelWaiter这个方法做了两件事,将队列中为取消状态的结点(status >0)踢出队列,释放等待队列头结点(status 改为0),唤醒头结点的后继结点。我们继续看看它如何实现:

private long cancelWaiter(WNode node, WNode group, boolean interrupted) {
        if (node != null && group != null) {
            Thread w;
            // node为要取消的结点,状态改为CANCELLED
            node.status = CANCELLED;
           // 如果group结点是cowait栈结构,则移除栈中被取消的结点
            for (WNode p = group, q; (q = p.cowait) != null;) {
                if (q.status == CANCELLED) {
                    U.compareAndSwapObject(p, WCOWAIT, q, q.cowait);
                    p = group; // restart
                }
                else
                    p = q;
            }
            //acquireWrite方法调用的时候,这个是为true
            if (group == node) {
                // 唤醒cowait栈中未取消的等待结点(这些结点都是读模式也就是获取读锁线程)
                // 因为它们的头头group要被取消了,不唤醒可能就没人唤醒它们了
                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;      
                    //找到node的一个未被取消的后继结点
                     while ((succ = node.next) == null ||
                           succ.status == CANCELLED) {
                        WNode q = null;    // find successor the slow way
                        //这种和AQS里面一样,从尾部往前遍历找node未取消的后继结点,
                        //结点插入时,因prev指针比next先设置,所以next为空时,后继结点未必不存在,只是未来得及设置next而已
                        for (WNode t = wtail; t != null && t != node; t = t.prev)
                            if (t.status != CANCELLED)
                                q = t;     // don't link if succ cancelled
                        //确保node的后继结点是有效的(未取消)
                        if (succ == q ||   // ensure accurate successor
                            U.compareAndSwapObject(node, WNEXT,
                                                   succ, succ = q)) {
                            // 如果node为尾结点,它被取消,那么CAS修改尾指向node的前驱结点
                            if (succ == null && node == wtail)
                                U.compareAndSwapObject(this, WTAIL, node, pred);
                            break;
                        }
                    }
                    // 将node踢出队列,node的前驱结点指向它的后继结点
                    // 有一点不理解,为什么没有取消node的prev指针?
                    if (pred.next == node) // unsplice pred link
                        U.compareAndSwapObject(pred, WNEXT, node, succ);
                     //唤醒node的后继结点,因为node被取消,不去唤醒,就没人去唤醒后继结点succ了
                     if (succ != null && (w = succ.thread) != null) {
                        succ.thread = null;
                        U.unpark(w);       // wake up succ to observe new pred
                    }
                    //这里几行代码是将node取消的前驱结点踢出,找到有效前驱结点。
                    if (pred.status != CANCELLED || (pp = pred.prev) == null)
                        break;
                    node.prev = pp;        // repeat if new pred wrong/cancelled
                    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) {
                for (WNode t = wtail; t != null && t != h; t = t.prev)
                    if (t.status <= 0)
                        q = t;
            }
            // 如果头还是当年的那个头,那就唤醒它的后继结点
            if (h == whead) { 
                //后继结点存在,头结点既不是取消状态也不是等待状态,写锁已释放
                //后面两个条件没理解,看来读锁再来
                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;
    }

唤醒头结点的后继结点:

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

写锁释放

写锁释放就比较简单,涉及到动作没获取时那么夸张,就两点:1、清除邮戳stamp中写锁占有状态,2、唤醒队列中头结点的后继结点去拿锁。

    public void unlockWrite(long stamp) {
        WNode h;
        // 加写锁的时候,会返回一个邮戳stamp,里面包含了写锁状态
        // 如果邮戳状态变了,或者写锁未被占用,报错
        if (state != stamp || (stamp & WBIT) == 0L)
            throw new IllegalMonitorStateException();
        // 写锁在stamp的第八位,加上WBIT就是将第八位变为0,清除写锁,同时stamp会进一位
        state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
        //释放锁后,唤醒队列中头结点的后继结点
        if ((h = whead) != null && h.status != 0)
            release(h);
    }
到此写锁看完了,我们发现写锁还是独占模式,而且不像ReentrantReadWriteLock支持锁重入,也不支持Condition进行精细化地控制锁。当写锁被占的时候,后面线程想获取写锁,经过大量地自旋才会挂起入等待队列。ReentrantReadWriteLock虽然获取不到锁也会自旋等待,但是StampedLock自旋次数更多,尽最大可能去避免线程挂起。

读锁的获取

读锁在入队时,发现尾结点是WMODE(写模式)时,如下图,如果WNODE1是写模式,且是队尾时,获取读锁线程阻塞入队,WNODE2就直接插入到后面成为新的队尾,为读模式;
如果又来了个想获取读锁的线程,它发现队尾时读模式,那么它就不会再加入到WNode2后面形成新队尾,而是以WNode2为头形成一个栈,如图中cowait1;
如果又来了一个线程还是拿读锁,那么它继续入栈,如图中cowait2
这样设计,将连续读线程放到一个栈中,有一个好处:前面竞争写锁线程,出队后,可以同时唤醒栈中所有线程去获取写锁,效率高

    public long readLock() {
        long s = state, next;  // bypass acquireRead on common uncontended case
        // 如果队列中没有排队线程,读锁获取没有达到上限,那就CAS 读锁数量加1
        // 否则又是通过acquireRead自旋获取锁,失败后入队,原理上和写锁大差不差
        return ((whead == wtail && (s & ABITS) < RFULL &&
                 U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
                next : acquireRead(false, 0L));
    }

acquireRead方法涉及到自旋获取锁,失败后入队或取消操作,以及对头结点的cowait栈的挂起线程协助唤醒操作,总之巨鸡儿长,能一行一行看完的都是勇士o.o。

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;;) {
                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) {
                    //自旋次数大于0,继续自旋,随机扣除自旋次数
                    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;
                        }
                        //自旋次数小于0,则初始化它
                        spins = SPINS;
                    }
                }
            }
        }
        // 一阶段自旋失败后,就为入队作准备了
        //如果队列未初始化就初始化它
        if (p == 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(RMODE, p);
        //队列为空或者队尾不是读模式,则直接插入队列
        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;
            }
        }
        // 走到这里,说明队尾为读模式,此时新结点加入到队尾结点的栈中
        //p结点不动,新增结点每次都是插在p的后面,作为后继结点
        // p->wc1   ==>    p->wc2->wc1
        //从这里我们可以看出,只有前置结点为写模式,当前node才能加到CLH队列中,否则,就加入到前置结点的栈中
        else if (!U.compareAndSwapObject(p, WCOWAIT,
                                         node.cowait = p.cowait, node))
            node.cowait = null;
        // CAS 成功,当前结点成功入栈,注意下面这个自旋专门处理入栈后的读结点,
        // 因为入CLH队列的读结点在上面直接break了。会走下下面那个自旋
        //这个自旋作用:
        //1、唤醒头结点上cowait栈的挂起线程,
        //2、决定当前线程结点是挂起还是取消(当然在这之前还有一次获锁机会)
        else {
            for (;;) {
                WNode pp, c; Thread w;
                // 如果头结点存在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);
                //注意:这里的p仍然指向的是尾结点,因为当前结点只是入了尾结点所在的栈,不是加入到队列中,替换为尾结点
               //尾结点前置结点等于头说明队列只有一个结点,或者队列为空,此时当前node应该尝试一下去获锁
                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;
                    //前两个条件用于判断队列没有等到线程了,则当前线程又有了资格获锁,没必要排队了
                    //后一个条件是队尾被取消了,因为当前node是挂在队尾为头的栈上啊,队尾一取消,也挂不住了
                    //因此node置为null,退出当前循环,重新去外层经历循环获锁,入队等操作
                    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;
                    //此处线程判断线程是否该挂起,这巴拉巴拉条件,可见作者是真的不想让线程挂起o.o
                    //在队列无变化的前提下,CLH队列有其它结点在排队,或者写锁还被占用
                    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);
                }
            }
        }
    }
   //能走到这里,是上面大循环中,读模式结点成功加入到CLH队列后,break才到这的 
   //这个自旋就是为了处理CLH队列上刚插入读模式结点的,而刚插入到栈上的读模式结点,在上个循环里面的一个自旋中已处理过了
    for (int spins = -1;;) {
        WNode h, np, pp; int ps;
        //头结点等于当前结点的前驱结点,说明队列中只有当前结点在排队,再次尝试获读锁
        //注意啊:这里的p和上个循环的p所指的不一样,这里是指向尾结点的前置结点
        //因为能走到这里,说明node已经入了CLH队列成为了新的尾结点,p.next=node
        if ((h = whead) == p) {
            //这部分代码和写锁里面差不多,初始化自旋值
            if (spins < 0)
                spins = HEAD_SPINS;
            // 如果自旋spins次数后,仍然无法获取锁,那就将它扩大2倍
            else if (spins < MAX_HEAD_SPINS)
                spins <<= 1;
            //这里开始自旋获锁了
            for (int k = spins;;) { // spin at head
                long m, s, ns;
                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;
                    //当前node获取锁成功后,如果是存在cowait栈,就唤醒栈上等待的线程
                    //因为读锁是可以多个线程去获取的,因写锁被占,其它获取读锁线程会加入到栈中挂起
                    while ((c = node.cowait) != null) {
                        if (U.compareAndSwapObject(node, WCOWAIT,
                                                   c, c.cowait) &&
                            (w = c.thread) != null)
                            U.unpark(w);
                    }
                    return ns;
                }
                //或锁失败,写锁仍被占,就随机自旋次数减一,为0时退出循环
                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) {
            //但node的前置结点变了,这说明了旧的前置结点被取消了,之前在获取写锁中,我们也可以看到多个清理被取消结点的过程
            if ((np = node.prev) != p) {
                if (np != null)
                    //设置新前置结点的next指针
                    (p = np).next = node;   // stale
            }
            //如果前置结点状态还不是等待状态,也不是取消状态,那就把它设置为等待状态
            // 这样当前node才能安心挂起,到时候有人唤醒它
            //类似于AQS中,CLH队列中结点的线程在挂起前,会给前置结点设置singal状态一样
            // 都是为了让前置结点在释放锁的时候,唤醒自己
            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;
                }
            }
            // 这个时候,终于要处理当前线程是挂起还是响应超时或中断取消了
            // 都是一样的代码,没必要讲了,能看到这里,你也是牛逼大大的o.o
            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);
            }
        }
    }
}

读锁的释放:

    public void unlockRead(long stamp) {
        long s, m; WNode h;
        //因为涉及到共享变量state的操作,不想加锁,那就老老实实CAS+自旋
        for (;;) {
            // 罗里吧嗦地校验邮戳的状态是否已变化,以及是否持有锁,写锁是否还存在
            // 因为存在写锁时,不可能有读锁,所以释放不存在锁要报错
            if (((s = state) & SBITS) != (stamp & SBITS) ||
                (stamp & ABITS) == 0L || (m = s & ABITS) == 0L || m == WBIT)
                throw new IllegalMonitorStateException();
            // 如果未超过读锁数量上限
            // 获取读锁时,若超过数量上限,不会拒绝,用readerOverflow来记录额外的读锁数
            if (m < RFULL) {
                if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) {
                    // 所有读锁释放完了才会唤醒
                    // 因为在前介绍的CLH队列中,读模式结点后面必然是写模式结点(连续的读模式结点会加到cowait栈中)
                    // 既然是写模式结点,写读互斥,那么只有读锁释放完了,才能拿到写锁,所以提前唤醒它没得意义
                    if (m == RUNIT && (h = whead) != null && h.status != 0)
                        release(h);
                    break;
                }
            }
            // 处理读锁超过上限时,锁释放
            else if (tryDecReaderOverflow(s) != 0L)
                break;
        }
    }

tryDecReaderOverflow这个方法用于释放时,获取的读锁已经超过上限的场景。

说句实话,我不大理解读锁超过上限后,为啥还搞个readerOverflow这个玩意来记录超出的锁,毕竟你邮戳状态可是Long型的啊,64位啊,又不像AQS,锁状态是Int型的,32位不够用,为啥不直接把读锁从低7位阔大呢?StampedLock的state前面好多位都没有用到,希望有懂的指点一下。
还有一点不理解,为什么tryDecReaderOverflow还要根据一定周期,做线程让步,给谁让?
    private long tryDecReaderOverflow(long s) {
        // assert (s & ABITS) >= RFULL;
        if ((s & ABITS) == RFULL) {
            if (U.compareAndSwapLong(this, STATE, s, s | RBITS)) {
                int r; long next;
                // 超出的读锁由readerOverflow计数,释放时减一
                if ((r = readerOverflow) > 0) {
                    readerOverflow = r - 1;
                    next = s;
                }
                else
                    next = s - RUNIT;
                 state = next;
                 return next;
            }
        }
        //根据线程随机数与上7,如果等于0,就线程让步
        // 为啥要让啊?让给谁啊?莫名其妙的,看不懂
        else if ((LockSupport.nextSecondarySeed() &
                  OVERFLOW_YIELD_RATE) == 0)
            Thread.yield();
        return 0L;
    }

乐观读锁

前面说的读锁获取都是悲观读锁啊,与写锁时互斥的,持有读锁的时候,不能获取写锁,持有写锁的时候会阻塞获取读锁。乐观锁就不一样,适用于读多写少的场景,根据tryOptimisticRead的注释,可以了解到,它必须和boolean validate(long stamp)方法一起用才行,不然那就是瞎乐观了啊!

获取乐观锁时,如果存在写锁,那么乐观锁是无效的,需要用validate(long stamp)验证,但是持有悲观读锁时,不影响乐观锁获取,获取乐观锁时,不影响写锁获取,只不过写锁获取后,stamp肯定是变了的,乐观锁也会失效。

    public long tryOptimisticRead() {
        long s;
        // 获取乐观锁,写锁不能被占用
        return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
    }

//用来验证tryOptimisticRead中返回的邮戳是否变化,即验证乐观锁是否有效

    public boolean validate(long stamp) {
        U.loadFence();
        return (stamp & SBITS) == (state & SBITS);
    }

StampedLock锁转换方法(简单翻译一下注释了)

tryConvertToReadLock(long stamp)

转为读锁

如果stamp记录的是写锁,则释放写锁,去获取读锁
如果stamp记录的是读锁,那就直接返回stamp
如果乐观读锁,当读锁可获取时,获取读锁,返回stamp

tryConvertToWriteLock(long stamp)

转为写锁

如果stamp记录的是读锁,则写锁能获取的情况下,释放读锁,获取写锁
如果stamp记录的是写锁,那就直接返回stamp
如果乐观读锁,当写锁可获取时,获取写锁,返回stamp

tryConvertToOptimisticRead(long stamp)

转为写锁

如果stamp记录的是其它锁,释放锁,获取乐观锁
如果乐观锁,那就直接返回stamp

小总

扯了这么多,发现StampedLock性能高,是通过无锁,CAS+大量自旋来得到的,牺牲了CPU性能,这一点是所有高性能工具的共同点,同时它尽最大可能避免线程的挂起,这个很耗时。
StampedLock的写锁和悲观读是互斥,但是它和ReentrantReadWriteLock相比还是有很多不同:

  • StampedLock不支持重入锁
  • StampedLock不支持Condition,精细化操作锁
  • StampedLock支持不同锁之间的转换
  • StampedLock支持乐观读
  • StampedLock未基于AQS实现,等待队列是CLH队列+cowait栈构成

虽然有很多不同,但是大致思想:比如队列唤醒机制,以及队列结点取消操作等,都和AQS是大差不差的。
告辞!!!!


SanPiBrother
24 声望3 粉丝

菜鸡的救赎之路