JUC之JDK自带锁StampedLock

一、初见

StampedLock是JDK 1.8的一把新锁,同样出自Doug Lee之手。这货高级了,出身显赫、自带光环,有着光辉的使命。她是一把不一样的锁,前面我们所整理过的两把锁(ReentantLock&ReentrantReadWriteLock)都是基于AQS框架实现,同时又都具有可重入性(当然可重入性不是由AQS框架带来的)。然而她却与众不同,她是读写锁,她是把乐观锁,她是基本于时间戳实现。她优化了ReentrantReadWriteLock提供更高更高的OPS,她意在替代ReentrantReadWriteLock提供高效易用的读写锁。

A capability-based lock with three modes for controlling read/write access. The state of a StampedLock consists of a version and mode. Lock acquisition methods return a stamp that represents and controls access with respect to a lock state; “try” versions of these methods may instead return the special value zero to represent failure to acquire access. Lock release and conversion methods require stamps as arguments, and fail if they do not match the state of the lock.

二、走进不一样

然而她没有基于AQS框架实现,而是自己实现了CLH队列;她不用Integer表示状态了,改用Long表示状态。她就是如此出众,让我们走进StampedLock的源码,窥探她美貌与才华。

首先我自己并没有真正用过把锁,不过接下来所有也不是虾扯蛋,据说我曾经看过的一些资料和StampedLock源码来梳理。我尽管可能多的把我看过的一些有价值的资料罗列出来。

注释里提到它是通过提供一个stamp来控制访问权限,当然对于这个我的理解不够,需要理解清楚的你可以分享。正也因为它的这个特性使得它变得比较脆弱,在极端条件容易抛出异常。
StampedLock相比于ReentrantReadWriteLock多一个tryOptimisticRead()方式提供乐观读锁,实现一种只在读前做读锁权限测试,即是不用读操作过程中加锁。对这个我们可以看看她的注释里的示例,这能让大家更加清晰的理解。

2.1 她自己重新实现了CLH队列

正因为她重新实现了CLH队列,所以她的源码非常复杂,也就没有ReentrantReadWriteLock那么清晰了。当然她俩要是都差不多,那么也就没有要必要如此大动干戈了,重新写出一个类来。

先看两个术语:
1. 悲观锁
悲观锁,它很悲观,有点被害妄想症。时时刻刻认为总人会跟它同时操作造成脏读,所以它很没有安全感,所以它每个操作都会加锁来保证同步。
2. 乐观锁
乐观锁,它又非常乐观,它总觉得它非常幸运。只要是他去数据就不会有人来修改,所以它并不在读数据时加锁。

按国际惯例此处应该是先来看,不过现在还不是看代码的时候。要是把源码贴出来了,目测您都不想再继续看下去了,要是不贴源码有些地方讲起来感觉空落落的。

2.2 让我困惑的State

先看一下state,前面提过她把state的精度从Integer升级到了Long。恕在下愚钝,真没看出来她改用Long的用意。7bit的长度表示读状态,第8个bit用来区分读写锁。如此说来,那她只允许128线程同时获得读锁吗?答案自然不是这样。StampedLock把超出127后的部分,即溢出部分放到其它位置了。StampedLock弄出另一个还记录溢出数,readerOverflow,用来记录溢出部分的数量。

在StampedLock的注释中提到,StampedLock的state由Version和Model两部分组成
对于左边第一位也就算是Model位,后7位当成Version位。当Model位为1时,表示写模式;Version位记录当前读者数。

实在没看出StampedLock用Long的原由,希望知道的你可以分享。

2.3 重新来看看CLH队列

接下来来看看CLH部分的内容。
从CLH的部分看最明显差异是名字改成WNode,更强调wait;其次多一个读者获取读锁的等待链表;然后加了两个属性。

static final class WNode {
    volatile WNode prev;
    volatile WNode next;
    volatile WNode cowait;    // list of linked readers
    volatile Thread thread;   // non-null while possibly parked
    volatile int status;      // 0, WAITING, or CANCELLED
    final int mode;           // RMODE or WMODE
    WNode(int m, WNode p) { mode = m; prev = p; }
}

我们已然知道StampedLock把CLH队列与Lock业务逻辑参合在一起实现,其实是很难单独把CLH队列拿出来看。更准确的说,就是基本都是CLH队列的实现,它占很大的篇幅。逻辑上也比较复杂,代码上又一味的追求简洁,使得她的源码读得起来就没那么自然和顺畅了。下面举两个过度追求简洁例子。

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

// 这么看其实还是很难看明白的,但稍微变换一下就易懂很多了,这就是代码的艺术。
public long readLock() {
    long next, s=state;
    if(whead == wtail && (s & ABITS) < RFULL) {
        if(U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) 
            return next;
    }
    return acquireRead(false, 0L);
}

-----------------------------------------------------------------------------
// acquireRead() 源码,#1144
if ((m = (s = state) & ABITS) < RFULL ?
                        U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
                        (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L))
                        return ns;
// 修改后
boolean v;
if ((m = (s = state) & ABITS) < RFULL) 
    v = U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT);
else 
    v = (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L);
if(v) return ns;

我们知道不管是acquireRead()或者acquireWrite()其实流程上都差不太多,只是acquireRead()相对复杂一点而已。它们都带有两个for循环,它们即是流程控制也是自旋锁的实现方式。

2.3.1 第一个for循环

而且两个方法实现的功能都非常相似,它们的要点是
1. 初始化队列
2. 新节点入队
3. 取得关键节点

2.3.2 第二个for循环

  1. 校验新节点的状态

我个人认为StampedLock比较难读有两个原因,第一个是前面提的过于简洁;第二个是流程控制。由于StampedLock是自旋锁,因而它是自身需要引入一些循环来支持这个特征。这一块可以不看的。

对于流程控制的话,即是采用了大量的if-else if。其实单纯只是if-else if也不能说它难读,如果再上面加个循环的话,那就变化相当复杂了。

先来看看acquireWrite()的第一个for循环。有请源码:

// StampedLock#acquireLock
WNode node = null, p;
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)
        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;
    }
}

想看这个流程其实需要一些假设条件,我们先假设
1. 刚刚创建这个锁
2. 已经有一个以上的读者获取读锁

下面CLH队列是新节点进入CLH队列的流程,准确的说前3步是初始队列,第4步是入队。
《JUC之JDK自带锁StampedLock》
《JUC之JDK自带锁StampedLock》

对于acquireRead()方面的内容不再继续看,因为它比较复杂。哈哈哈

三、再见

ReentrantReadWriteLock很容易出现写饥饿,在读多写少的时候。StampedLock的出现最主要目标就是优化这个场景,在写少的时候情况采用乐观锁的方式来解决写饥饿的问题。即是以”一种乐观的心态“来读数据,减少锁资源的占用。

在ReentrantReadWriteLock只允许用户从锁降级,即从写降成读,但是读不能变成写。
但是呢,但由于StampedLock也完全不支持,即不能升级,也不能降级。因为它不是递归锁,不能在写锁里面去调用带读锁的方法。

    原文作者:JUC
    原文地址: https://blog.csdn.net/zteny/article/details/56412434
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞