StampedLock

299 阅读3分钟

介绍

StampedLock为JDK1.8引入,可以认为它是读写锁(关于读写锁的介绍请看前一篇文章)的改进。读写锁使读与读之间并发,但读与写之间是阻塞的。如果有大量读线程会导致写线程一直阻塞从而可能引起写线程的“饥饿”。StampedLock则提供了一种乐观读策略,类似无锁操作,不会阻塞写线程。

StampedLock使用示例


public class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();
    //对x,y的修改,使用写锁
    void move(double deltaX, double deltaxY){
        //申请获取写锁,返回一个"邮戳"
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaxY;
        } finally {
            //使用申请写锁获得的"邮戳"进行释放锁
            sl.unlockWrite(stamp);
        }
    }
    //只读方法,先使用乐观读,失败则转化为悲观读
    double distanceFromOrigin(){
        //尝试获取一个乐观读
        long stamp = sl.tryOptimisticRead();
        double currentX = x, currentY = y;
        //判断validate在读取过程中是否发生修改
        if(!sl.validate(stamp)){
            //发生了冲突(在乐观读的时候有其它线程进行了修改),则升级锁级别为悲观锁进行读
            //另外一种处理方式:也可以像处理CAS操作那样在一个死循环中一直使用乐观读,直到成功为止
            stamp = sl.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                //释放悲观读锁通过stamp
                sl.unlockRead(stamp);
            }

        }
        //乐观读获取成功则进行相关业务计算
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}

通过上面的例子可以看出StampedLock通过引入了乐观读来提高系统并行。

StampedLock申请获取读锁相关API

//获取读锁(必要时阻塞,不响应中断)
public long readLock()
//尝试获取读锁,不阻塞,立即返回
public long tryReadLock()
//超时限制的获取读锁,响应中断(返回0则表示锁不可用,例如规定时间内未获取到)
public long tryReadLock(long time, TimeUnit unit)
    throws InterruptedException
//获取读锁,必要时阻塞,响应中断
public long readLockInterruptibly() throws InterruptedException

StampedLock内部实现思想

StampedLock内部实现基于CLH锁。CLH锁是一种自旋锁,它保证没有饥饿发生,并且可以保证FIFO的服务顺序。

CLH锁的基本思想:

volatile happens-before (3).png

CLH锁会维护一个线程等待队列,申请锁但没成功的的线程会记录在这个队列中,每个节点都会保存一个locked标记位(判断当前线程是否释放锁).过程大致如下:

1.创建一个Node,将locked设置为true(表示需要获取锁)

2.线程Node添加到队列的尾部并获取一个指向其前序节点的引用

3.线程在其前序节点locked字段字段进行自旋,直到前序节点释放锁(locked设置为false)

4.前序节点将locked设置为false释放了锁,同时也会回收它的前序节点

StampedLock基于这种思想,但是实现上更加复杂。

StampedLock内部维护的等待链表队列结构如下:

//每个WNode代表一个等待线程
/** Wait nodes */
static final class WNode {
    volatile WNode prev;
    volatile WNode next;
    //d读节点链表
    volatile WNode cowait;    // list of linked readers
    volatile Thread thread;   // non-null while possibly parked
    //有没有取消 0:WAITING 1:CANCELLED
    volatile int status;      // 0, WAITING, or CANCELLED
    //读写模式 0:读 1:写
    final int mode;           // RMODE or WMODE
    WNode(int m, WNode p) { mode = m; prev = p; }
}

//CLH队列头部(指向链表头部)
/** Head of CLH queue */
private transient volatile WNode whead;
//CLH队列尾部(指向链表尾部)
/** Tail (last) of CLH queue */
private transient volatile WNode wtail;

//当前锁的状态
/** Lock sequence/state */
private transient volatile long state;

上面看到有一个重要字段state,用来表示当前锁的状态。它是一个64位的long整数,倒数第8位表示写锁状态(为1表示写锁占用)。末尾7位表示当前正在读取的线程数量(如果溢出,会使用readerOverflow进行统计),前面56位表示写锁释放的次数。

state变量结构如下:

volatile happens-before (4).png

state初始化:


/** The number of bits to use for reader count before overflowing */
private static final int LG_READERS = 7;
private static final long WBIT  = 1L << LG_READERS;

//初始化将第8位设置为1(...1 0000 0000), ...表示前面高55位
// Initial value for lock state; avoid failure value zero
private static final long ORIGIN = WBIT << 1;

写锁的申请和释放

/**
 * Exclusively acquires the lock, blocking if necessary
 * until available.
 *
 * @return a stamp that can be used to unlock or convert mode
 */
public long writeLock() {
    long s, next;  // bypass acquireWrite in fully unlocked case only
    return ((((s = state) & ABITS) == 0L && //是否有读写锁占用,没有的话等于0
             U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
             //写锁标志位设置为1成功返回next,否则通过acquireWrite进行锁的获取
            next : acquireWrite(false, 0L));
}



/**
 * If the lock state matches the given stamp, releases the
 * exclusive lock.
 *
 * @param stamp a stamp returned by a write-lock operation
 * @throws IllegalMonitorStateException if the stamp does
 * not match the current state of this lock
 */
public void unlockWrite(long stamp) {
    WNode h;
    //检查申请锁的邮戳是否发生了改变
    if (state != stamp || (stamp & WBIT) == 0L)
        throw new IllegalMonitorStateException();
    //将state写标志位设置为0并增加释放锁的次数(如果释放锁的次数高56的限制,则设置为ORIGIN)
    state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
    //头节点非空并且status不等于0(不等于0表示未释放锁)
    if ((h = whead) != null && h.status != 0)
        //唤醒队列中下一个线程
        release(h);
}

读锁的申请和释放


/**
 * Non-exclusively acquires the lock, blocking if necessary
 * until available.
 *
 * @return a stamp that can be used to unlock or convert mode
 */
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)) ? //state读线程数量增加
            //申请成功返回next,申请失败则 调用acquireRead继续申请
            next : acquireRead(false, 0L)); 
}

/**
 * If the lock state matches the given stamp, releases the
 * non-exclusive lock.
 *
 * @param stamp a stamp returned by a read-lock operation
 * @throws IllegalMonitorStateException if the stamp does
 * not match the current state of this lock
 */
public void unlockRead(long stamp) {
    long s, m; WNode h;
    //自旋
    for (;;) {
        //检查当前state和stamp的状态是否正确
        if (((s = state) & SBITS) != (stamp & SBITS) ||
            (stamp & ABITS) == 0L || (m = s & ABITS) == 0L || m == WBIT)
            throw new IllegalMonitorStateException();
        if (m < RFULL) {
            // 读线程数量小于最大值则state标志的读线程数量减1
            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;
    }
}



StampedLockd的readLock()的缺陷

StampedLockd的readLock()会出现疯狂占用CPU的情况,如下示例所示:


public class StampedLockCPUTest {
    static Thread[] holdCpuThreads = new Thread[3];
    static final StampedLock lock = new StampedLock();

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                long readLong = lock.writeLock();
                //模拟一直等待不释放锁
                LockSupport.parkNanos(600000000000L);
                lock.unlockWrite(readLong);
            }
        }).start();

        Thread.sleep(100);

        for (int i = 0; i< 3; i++) {
            holdCpuThreads[i] = new Thread(new HoldCPUReadThread());
            holdCpuThreads[i].start();
        }

        Thread.sleep(10000);
        //线程中断后,会占用CPU
        for(int i = 0; i < 3; i++){
            holdCpuThreads[i].interrupt();
        }
    }

    public static class HoldCPUReadThread implements Runnable {
        @Override
        public void run() {
            long lockr = lock.readLock();
            System.out.println(Thread.currentThread().getName() + "获得读锁");
            lock.unlockRead(lockr);
        }
    }
}

上面示例可以看出,三个读锁最终会被挂起,挂起线程使用Unsafe.park()函数,在park()函数遇到中断的时候,不会抛出异常,而是直接返回。则程序会继续自旋,当又到park()函数的时候,由于中断标识依然存在,则park()函数又直接返回,程序又继续自旋...。这样自旋就陷入了死循环(达不到退出条件),就会出现疯狂占用CPU的情况.

对于这个情况,需要对中断进行处理,例如退出,抛出异常等。但在jdk8中并未处理,需要特别注意下.

Java高并发程序设计(第2版)