StampedLock浅析(一)

目录

 

一、概述

二、部分常量介绍

三、写锁 

获取

释放

四、乐观读锁

五、读锁

获取

释放


一、概述

一种基于功能的锁,具有三种模式来控制读/写访问。 StampedLock的状态由版本和模式组成。 锁获取方法返回一个戳记(stamp),表示锁状态的访问控制; 这些方法的“尝试”版本可能会返回特殊值零,以表示无法获取访问权限。 锁释放和转换方法需要使用戳记作为参数,如果它们与锁的状态不匹配,则会失败。

三种模式是:

  • 写。方法writeLock可能会阻塞以等待独占访问,返回可以在方法unlockWrite使用的戳记stamp以释放锁。还提供了tryWriteLock不定时版本和定时版本。 当锁保持在写模式时,将无法获得任何读锁,并且所有乐观的读验证都将失败。

  • 读。 方法readLock可能会阻塞以等待非独占访问,返回可以在方法unlockRead使用的戳记stamp以释放锁。 也提供了tryReadLock不定时版本和定时版本。

  • 乐观的读。 仅当锁当前未处于写入模式时,方法tryOptimisticRead返回非零戳。 如果自获取给定戳记以来未在写模式下获取锁,则validate方法将返回true。 可以将这种模式视为读取锁的极弱版本,写可以随时将其破坏。 在短的只读代码段中使用乐观模式通常可以减少争用并提高吞吐量。 但是,它的使用本质上是脆弱的。 乐观的读取部分应仅读取字段并将其保存在局部变量中,以供验证后使用。 在乐观模式下读取的字段可能完全不一致,因此仅当您足够熟悉数据表示以检查一致性和/或重复调用方法validate()时,用法才适用。 例如通常需要这些步骤当首先读取对象或数组引用,然后访问其字段,元素或方法之一时。

此类还支持有条件地在三种模式之间提供转换的方法。 例如,方法tryConvertToWriteLock尝试“升级”模式,如果(1)已经处于写入模式(2)处于读取模式并且没有其他读取器,或者(3)处于乐观模式并且该锁可用,则返回有效的写入戳记。 这些方法的形式旨在帮助减少在基于重试的设计中原本会发生的某些代码膨胀。

StampedLocks设计为在线程安全组件的开发中用作内部实用程序。 它们的使用取决于对它们所保护的数据,对象和方法的内部属性的了解。 它们不是可重入的(指的是写锁),因此锁定的主体不应调用可能尝试重新获取锁的其他未知方法(尽管您可以将戳记传递给可以使用或转换它的其他方法)。 读取锁定模式的使用依赖于相关联的代码段无副作用。 未经验证的乐观阅读部分无法调用未知的方法来容忍潜在的不一致。 戳记使用有限表示,并且不是加密安全的(即,有效的戳记可能是可猜测的)。 戳记值可能会在(不早于)连续运行一年后回收。 未经使用或验证而持有超过该期限的戳记可能无法正确验证。 StampedLocks是可序列化的,但始终反序列化为初始未锁定状态,因此它们对于远程锁定没有用处。

StampedLock的调度策略并不总是喜欢读而不是写,反之亦然。 所有“尝试”方法都是尽力而为的,不一定符合任何调度或公平性策略。 任何用于获取或转换锁的“尝试”方法的零返回值都不会携带有关锁状态的任何信息。 随后的调用可能会成功。

因为它支持跨多种锁定模式的协调使用,所以此类不直接实现Lock或ReadWriteLock接口。 但是,在仅需要关联功能集的应用程序中,StampedLock可以被视为asReadLock() , asWriteLock()或asReadWriteLock() 。

二、部分常量介绍

 

常量介绍
常量字段含义十进制二进制其它高位值
WBIT 写锁标识位128     1000 00000
RBITS最大读取器计数值,只是临时值1270111 11110
RFULLstate前7位表示的读锁个数,真实最大值,超过的部分存到readerOverflow中1260111 11100
ABITS 2551111 11110
SBITS参与用于记录写锁获取次数的与运算-1281000 00001
ORIGINstate初始值2561 0000 00000

 StampedLock使用第8位表示写锁是否被占用,1表示占用,0表示未占用。使用前7位表示读锁。如果写锁和读锁都未被占用,则说明8位二进制数全为0,也就是state & ABITS == 0L,因为ABITS二进制8位全是1,只要state8位中有一位为1,也就是被读锁或写锁占有,则结果都不是0。

三、写锁 

获取

 public long writeLock() {
        long s, next;  // bypass acquireWrite in fully unlocked case only
        return ((((s = state) & ABITS) == 0L &&
                 U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
                next : acquireWrite(false, 0L));
    }

state & ABITS == 0L,表示读锁和写锁都未被占用。通过CAS更新state。更新操作位当前state加 二进制数【1000 0000】,一开始state为【0001 0000 0000】,加后为【0001 1000 0000】,

返回加之后的值。负责调用accquireWrite。

释放

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

1.state != stamp || (stamp & WBIT) == 0,说明state被修改过,或最高位已不是1。

2.如果当前戳记加上WBIT,如果结果为0,则将state置为初始值。不为0,则state为加后的结果。

解释:通过上述加锁后结果【0001 1000 0000】+【1000 0000】=【0010 0000 0000】,会发现释放写锁后第8位又变为了0。且第8位之后加1。为什么不减去WBIT变为初始值呢,这是为乐观读锁做铺垫。因为乐观读锁校验时之前,如果写锁被获取且释放为初始值ORIGIN,那就不知道是否有线程获取过写锁,就会发生并发问题。而使更高为加1,相当于留痕。这样就解决了CAS的ABA问题。可以看出,8位之后的值可以看做写锁获取次数。  加后结果为0是因为总共就64位,所以写锁一直累加就会为0。

四、乐观读锁

并没有真正获取锁,没有改过state值,所以也就没有释放锁的过程。乐观锁基本原理就是获取锁时记录state的写状态,然后在操作完成之后检查写状态是否有变化,因为写状态每次都会在高位留下记录。

    public long tryOptimisticRead() {
        long s;
        return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
    }

使用当前state和写锁标识位做与运算,WBIT二进制 为1000 0000,结果不为0只有一种情况,写锁被占用,返回0。这种情况验证一定为假,所以获取写锁后,再获取乐观读锁验证一定会失败。
写锁未被占用(可能有其它读锁),因为SBITS 后8位 1000 0000,然而当前state高位(第8位)不为1,所以s & SBITS值就是前8位0000 0000,后面的根据写锁之前获取过几次而定。也就是说返回值为写锁被获取的次数。也就是上述原理中所说的写锁状态。

乐观读锁校验:

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

1.tryOptimisticRead返回0,stamp & SBITS值为0,state第8位为1,state & SBITS 值为 0000 1000 0000 验证失败。简单理解state不会为0,所以等式不会成立。
2.tryOptimisticRead之前未获取写锁或写锁已释放:返回前8位0000 0000,这里以未获取过写锁为例,stamp & SBITS 值为:前12位 0001 0000 0000

  •  验证之前其它线程获取了写锁,此时state为 前8位1000 0000,state & SBITS为 前8位1000 0000,两者不相等,验证失败。
  •  验证之前其它线程获取了写锁且释放,此时state为 前12位 0010 0000 0000,state & SBITS为 前12位0010 0000 0000,两者不相等,验证失败。

再以写锁获取次数已达到高位全为1为例,stamp & SBITS 值为:前12位 1111 0000 0000,当然后面的高位也都是1,

  •  验证之前其它线程获取了写锁,此时state为 前8位1000 0000,state & SBITS为 前8位1000 0000,两者不相等,验证失败。
  •  验证之前其它线程获取了写锁且释放,以为释放会使高位加1,根据写锁释放代码可以看出,此时state将会被置为初始值 0001 0000 0000,state & SBITS为 前12位0001 0000 0000,两者不相等,验证失败。

所以再验证之前,只要获取过写锁,不管该写锁是否已被释放,都会验证失败。

更简单理解(不考虑tryOptimisticRead为0,这种情况一定失败):因为tryOptimisticRead返回的是写锁的状态且前8位0000 0000,SBITS前12位【1111 1000 0000】,后52位也全是1,可以看出如果获取过写锁,那么高位必然发生变化,而SBITS高位全是1,与运算后高位就是写锁状态。既然发生过变化,那么前后肯定是不相等的。如果有线程正持有写锁,第8位为1,就更不相等了。

五、读锁

获取

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

队列为空,无写锁,同时读锁未溢出,尝试获取读锁。获取成功state加1,返回state。其它情况调用acquireRead。可以看出,当state为125时,CAS将state变为126,再次获取时就溢出,acquireRead中对于溢出会调用tryIncReaderOverflow方法,将超出的部分存储到readerOverflow变量中。

    private long tryIncReaderOverflow(long s) {
        // assert (s & ABITS) >= RFULL;
        if ((s & ABITS) == RFULL) {
            if (U.compareAndSwapLong(this, STATE, s, s | RBITS)) {
                ++readerOverflow;
                state = s;
                return s;
            }
        }
        else if ((LockSupport.nextSecondarySeed() &
                  OVERFLOW_YIELD_RATE) == 0)
            Thread.yield();
        return 0L;
    }

 当前s为382【1 0111 1110】,s & ABITS为【0111 1110】等于RFULL值126,CAS将state从382设置为s | RBITS值383【1 0111 1111】,然后将readerOverflow加1,注意,这里然后又把state设回了382,且返回382。当再次获取读锁进入此方法时,s又是382,又仅仅将readerOverflow加1,其它不变。

可以看出,当state前 8位表示的读锁已达到RFULL值126时(state为382),之后每次获取读锁,都只是尝试通过先将state访问位的值设置为RBITS(383,前8位127)来指示增加自旋锁,然后进行更新然后释放(把state设回了382),以增加readerOverflow。这里CAS失败是因为并发。所以acquireRead中tryIncReaderOverflow外层有for循环。失败的线程会在成功设置的线程将state设回382后成功设置。 

释放

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

释放过程在没有溢出的情况下是通过减1来释放的,当溢出后则将readerOverflow变量减1。 

   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;
                if ((r = readerOverflow) > 0) {
                    readerOverflow = r - 1;
                    next = s;
                }
                else
                    next = s - RUNIT;
                 state = next;
                 return next;
            }
        }
        else if ((LockSupport.nextSecondarySeed() &
                  OVERFLOW_YIELD_RATE) == 0)
            Thread.yield();
        return 0L;
    }

当读锁数量大于等于126时,进入此方法。同tryIncReaderOverflow一样,先尝试将state从382设为383,如果readerOverflow大于0,则readerOverflow减1,state又设回382,返回382.如果readerOverflow不大于0,则直接state减1。因为当读锁个数恰好是126时,readerOverflow此时为0,就会触发else代码。那为什么不在读锁个数大于126再进入此方法,等于126时走外层state减1呢?没看出来。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值