JDK8的一种新的读写锁StampedLock

JDK8的一种新的读写锁StampedLock

JDK8新增一种新的读写锁StampedLock。一个最重要的功能改进就是读写锁中解决写线程饥饿的问题

StampedLock与ReentrantReadWriteLock

以StampedLock与ReentrantReadWriteLock两者比较为线索介绍StampedLock锁。关于ReentrantReadWriteLock锁参考文档ReentrantReadWriteLock源码解析。

StampedLock不基于AQS实现

之前包括ReentrantReadWriteLock,ReentrantLock和信号量等同步工具,都是基于AQS同步框架实现的。而在StampedLock中摒弃了AQS框架,为StampedLock实现提供了更多的灵活性。

StampedLock增加乐观读锁机制

先获取记录下当前锁的版本号stamp,执行读取操作后,要验证这个版本号是否改变,如果没有改变继续执行接下来的逻辑。乐观读锁机制基于在系统中大多数时间线程并发竞争不严重,绝大多数读操作都可以在没有竞争的情况下完成的论断。

//乐观读锁
stamp = lock.tryOptimisticRead();
//do some reading
if (lock.validate(stamp)) {
    //do somethinng
}

实际中,乐观读的实现是通过判断state的高25位是否有变化来实现的,获取乐观读锁也仅仅是返回当前锁的版本号。

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

StampedLock锁的状态和版本号

基于AQS实现的ReentrantReadWriteLock,高16位存储读锁被获取的次数,低16位存储写锁被获取的次数。
而摒弃了AQS的StampedLock,自身维护了一个状态变量state。

private transient volatile long state;

StampedLock的状态变量state被分成3段:

  1. 高24位存储版本号,只有写锁增加其版本号,而读锁不会增加其版本号;
  2. 低7位存储读锁被获取的次数;
  3. 第8位存储写锁被获取的次数,因为只有一位用于表示写锁,所以StampedLock不是可重入锁

关于状态变量state操作的变量设置:

private static final int LG_READERS = 7; //读线程的个数占有低7位

// Values for lock state and stamp operations
private static final long RUNIT = 1L; //读线程个数每次增加的单位
private static final long WBIT  = 1L << LG_READERS;//写线程个数所在的位置 1000 0000
private static final long RBITS = WBIT - 1L;//读线程个数的掩码 111 1111
private static final long RFULL = RBITS - 1L;//最大读线程个数
private static final long ABITS = RBITS | WBIT;//读线程个数和写线程个数的掩码 1111 1111
// Initial value for lock state; avoid failure value zero
//state的初始值。 1 0000 0000,也就是高24位最后一位为1,版本号初始值为1 0000 0000。锁获取失败返回版本号0。
private static final long ORIGIN = WBIT << 1;

StampedLock自旋

如下代码片段,可以看到StampedLock锁获取时存在大量自旋逻辑(for循环)。自旋是一种锁优化技术,在并发程序中大多数的锁持有时间很短暂,通过自旋可以避免线程被阻塞和唤醒产生的开销。
自旋技术对于系统中持有锁时间短暂的任务比较高效,但是对于持有锁时间长的任务是对CPU的浪费。

private long acquireWrite(boolean interruptible, long deadline) {
    for (int spins = -1;;) { // spin while enqueuing
          ....省略代码逻辑....
    }
    for (int spins = -1;;) {
        ....省略代码逻辑....
       for (int k = spins;;) { // spin at head
           ....省略代码逻辑....
}
private long acquireRead(boolean interruptible, long deadline) {
    for (int spins = -1;;) {
        ....省略代码逻辑....
        for (long m, s, ns;;) {
            ....省略代码逻辑....
        }
        ....省略代码逻辑....
        for (;;) {
            ....省略代码逻辑....
    for (int spins = -1;;) {
        for (int k = spins;;) { // spin at head
    ....省略代码逻辑....    
}

StampedLock的CLH队列

StampedLock的CLH队列是一个经过改良的队列,在ReentrantReadWriteLock的等待队列中每个线程节点是依次排队,然后责任链设计模式依次唤醒,这样就可能导致读线程全部唤醒,而写线程处于饥饿状态。StampedLock的等待队列,连续的读线程只有首个节点存储在队列中,其它的节点存储在首个节点的cowait队列中。

StampedLock唤醒读锁是一次性唤醒连续的读锁节点。

类WNode是StampedLock等待队列的节点,cowait存放连续的读线程。

static final class WNode {
    volatile WNode prev;
    volatile WNode next;
    //cowait存放连续的读线程
    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只有非公平模式,线程到来就会尝试获取锁。

你可能感兴趣的:(JDK8的一种新的读写锁StampedLock)