Java并发编程--锁原理之StampedLock锁

JDK 8 中新增的StampedLock锁的原理

(1). 概述

​  StampedLock锁是并发包中JDK 8版本新增的一个锁,提供了三种模式的读写控制.当调用try系列方法尝试获取锁时,会返回一个long型的变量stamp(戳记).这个戳记代表了锁的状态,可以理解为乐观锁中的版本.释放锁的转换锁时都需要提供这个标记来判断权限.

特点:

  • 所有获取锁的方法都会返回一个stamp戳记,0为失败,其他为获取成功
  • 所有释放锁的方法都需要一个stamp戳记,必须和之前得到锁时的stamp一致
  • 不可重入
  • 有三种访问模式
    • 乐观读
    • 悲观读
    • 写锁
  • 写锁可以降级为读锁
  • 都不支持条件队列
/** 等待链表队列,每一个WNode标识一个等待线程 */
static final class WNode {
    volatile WNode prev;
    volatile WNode next;
    volatile WNode cowait;    // 读模式使用该节点形成栈
    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; }
}


/** 锁队列状态,当处于写模式时第8位为1,读模式时前7为为1-126(读锁是共享锁,附加的readerOverflow用于当读者超过126时) */
private transient volatile long state;
/** 将state超过 RFULL=126的值放到readerOverflow字段中 */
private transient int readerOverflow;

(2). 写锁writeLock

​ 独占锁,不可重入.请求该锁成功后会返回一个stamp戳记变量来表示该锁的版本.

  • writeLock():当完全没有加锁时,绕过acquireWrite,否则调用acquireWrite入队列获取锁资源,
    • acquireWrite():入队自旋,并放到队列尾部,如果队列中只剩下一个结点,则在队头进一步自旋,最后会进入阻塞
  • unlockWrite():如果锁的状态与stamp相同,调用release()释放锁
    • release():唤醒传入节点的后继节点

(3). 悲观读readLock

​ 共享锁,不可重入

  • readLock():(队列为空&&没有写锁同时读锁数小于126&&CAS修改状态成功)则状态加1并返回,否则调用acquireRead()自旋获取读锁
    • acquireRead():首先是入队自旋,如果队尾不是读模式则放到队列尾部,如果是读模式,则放到队尾的cowait中。如果队列中只剩下一个结点,则在队头进一步自旋.如果最终依然失败,则Unsafe().park()挂起当前线程。
  • unlockRead():如果state匹配stamp,判断当前的共享次数,修改state或者readerOverflow

(4). 乐观读OptimisticRead()

​  在操作数据前并没有通过 CAS 设置锁的状态,仅仅是通过位运算测试

​  如果当前没有线程持有写锁,则简单的返回一个非 0 的 stamp 版本信息,获取该 stamp 后在具体操作数据前还需要调用 validate 验证下该 stamp 是否已经不可用,也就是看当调用 tryOptimisticRead 返回 stamp 后,到当前时间是否有其它线程持有了写锁,如果是那么 validate 会返回 0,否则就可以使用该 stamp 版本的锁对数据进行操作。

​  由于 tryOptimisticRead 并没有使用 CAS 设置锁状态,所以不需要显示的释放该锁。该锁的一个特点是适用于读多写少的场景,因为获取读锁只是使用位操作进行检验,不涉及 CAS 操作,所以效率会高很多,但是同时由于没有使用真正的锁,在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候可能其它写线程已经修改了数据,而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。

/**
 * 获取乐观读锁,返回stamp
 */
public long tryOptimisticRead() {
    long s;  //有写锁返回0,否则返回256
    return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}

/**
 * 验证从调用tryOptimisticRead开始到现在这段时间内有无写锁占用过锁资源,有写锁获得过锁资源则返回false. stamp为0返回false.
 * @return 从返回stamp开始,没有写锁获得过锁资源返回true,否则返回false
 */
public boolean validate(long stamp) {
    //强制读取操作和验证操作在一些情况下的内存排序问题
    U.loadFence();
    //当持有写锁后再释放写锁,该校验也不成立,返回false
    return (stamp & SBITS) == (state & SBITS);
}

(5). 锁转换

/**
 * state匹配stamp时, 执行下列操作之一. 
 *   1、stamp 已经持有写锁,直接返回.  
 *   2、读模式,但是没有更多的读取者,并返回一个写锁stamp.
 *   3、有一个乐观读锁,只在即时可用的前提下返回一个写锁stamp
 *   4、其他情况都返回0
 */
public long tryConvertToWriteLock(long stamp) {
    long a = stamp & ABITS, m, s, next;
    //state匹配stamp
    while (((s = state) & SBITS) == (stamp & SBITS)) {
        //没有锁
        if ((m = s & ABITS) == 0L) {
            if (a != 0L)
                break;
            //CAS修改状态为持有写锁,并返回
            if (U.compareAndSwapLong(this, STATE, s, next = s + WBIT))
                return next;
        }
        //持有写锁
        else if (m == WBIT) {
            if (a != m)
                //其他线程持有写锁
                break;
            //当前线程已经持有写锁
            return stamp;
        }
        //有一个读锁
        else if (m == RUNIT && a != 0L) {
            //释放读锁,并尝试持有写锁
            if (U.compareAndSwapLong(this, STATE, s, next = s - RUNIT + WBIT))
                return next;
        }else
            break;
    }
    return 0L;
}

/**
 * state匹配stamp时, 执行下列操作之一.
      1、stamp 表示持有写锁,释放写锁,并持有读锁
      2 stamp 表示持有读锁 ,返回该读锁
      3 有一个乐观读锁,只在即时可用的前提下返回一个读锁stamp
      4、其他情况都返回0,表示失败
 *
 */
public long tryConvertToReadLock(long stamp) {
    long a = stamp & ABITS, m, s, next; WNode h;
    //state匹配stamp
    while (((s = state) & SBITS) == (stamp & SBITS)) {
        //没有锁
        if ((m = s & ABITS) == 0L) {
            if (a != 0L)
                break;
            else if (m < RFULL) {
                if (U.compareAndSwapLong(this, STATE, s, next = s + RUNIT))
                    return next;
            }
            else if ((next = tryIncReaderOverflow(s)) != 0L)
                return next;
        }
        //写锁
        else if (m == WBIT) {
            //非当前线程持有写锁
            if (a != m)
                break;
            //释放写锁持有读锁
            state = next = s + (WBIT + RUNIT);
            if ((h = whead) != null && h.status != 0)
                release(h);
            return next;
        }
        //持有读锁
        else if (a != 0L && a < WBIT)
            return stamp;
        else
            break;
    }
    return 0L;
}

(6). 使用乐观读锁

 使用乐观读要保证以下顺序:

// 获取版本信息(乐观锁)
long stamp = lock.tryOptimisticRead();
// 复制变量到本地堆栈
copyVaraibale2ThreadMemory();
// 校验,如果校验失败,说明此处乐观锁使用失败,申请悲观读锁
if(!lock.validate(stamp)){
    // 申请悲观读锁
    long stamp = lock.readLock();
    try{
        // 复制变量到本地堆栈
        copyVaraibale2ThreadMemory();
    }finally{
        // 使用完之后释放锁
        lock.unlock();
    }
}

最后用一幅图加深理解

在这里插入图片描述

发布了141 篇原创文章 · 获赞 47 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_41596568/article/details/104076038
今日推荐