JAVA并发包(五):StampedLock

StampedLock是JDK8新增的API,是读写锁ReentrantReadWriteLock优化版,可以用来避免写饥饿。它使用版本戳和读写模式来控制并发访问,加锁会获得一个版本戳,然后解锁需要这个版本戳去匹配,匹配上才可以解锁。由于StampedLock的源码过于复杂,这里就不对源码做详细解读了,只是总结一下他的API特点以及使用场景。

一、三种锁模式

1. 悲观写锁

这是普通的排他锁,只能是一个线程获取到,但是不能重入,重入会引起死锁。看api写锁是不会阻塞,会一直自旋获取锁。调用writeLock()获取普通的写锁,调用tryWriteLock()方法会立即返回获取写锁的结果。

	public long writeLock() {
        long s, next; 
        // acquireWrite方法会做轮询获取,过于复杂不做详细解释
        return ((((s = state) & ABITS) == 0L &&
                 U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
                next : acquireWrite(false, 0L));
    }
	// 这个方法会马上返回加锁结果
	public long tryWriteLock() {
        long s, next;
        return ((((s = state) & ABITS) == 0L &&
                 U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
                next : 0L);
    }

2.悲观读锁模式

悲观读锁与写锁是互斥,但是读锁与读锁之间可以共享,获取悲观读锁的方法是readLock(),调用这个方法会首先进行自旋获取,如果多次自旋获取不了则阻塞线程。另外可以调用立即返回加锁结果的方法tryReadLock()

	public long readLock() {
        long s = state, next;  
        // acquireRead()方法首先会做自旋
        return ((whead == wtail && (s & ABITS) < RFULL &&
                 U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
                next : acquireRead(false, 0L));
    }
    
	public long tryReadLock() {
        for (;;) {
            long s, m, next;
            if ((m = (s = state) & ABITS) == WBIT)
                return 0L;
            else if (m < RFULL) {
                if (U.compareAndSwapLong(this, STATE, s, next = s + RUNIT))
                    return next;
            }
            // 这个分支会因为读锁数量大于了126个,则做溢出的处理,调用者不用关注这里
            else if ((next = tryIncReaderOverflow(s)) != 0L)
                return next;
        }
    }

3.乐观读模式

它适合多读特别少写的场景,注意这个少写,就是整个加乐观锁的过程中,写预计会出现特别少的次数。如果读取的过程中,写的次数过多,那么这个乐观锁其实就会失效,因为乐观锁不是可信的,能够被写锁破坏掉。乐观锁其实没有加锁,只是在读取前,它首先是调用tryOptimisticRead()获取到一个版本戳,然后再读取共享变量,最后在使用这些共享变量时再次验证这个版本戳是否有写锁改过。下面看看官方的例子吧

class Point {
  private double x, y;
  private final StampedLock sl = new StampedLock();
  double distanceFromOrigin() { 
	// 这里调用乐观锁,从源码中可以看到,它没有加锁,只是获取到一个版本戳
    long stamp = sl.tryOptimisticRead();
    // 这里可以理解读取共享的变量
    double currentX = x, currentY = y;
    // 最后使用前判断共享变量是否被写过(因为写的记录会被状态位记住,所以如果短期内有写过的话,版本戳肯定是匹配不上的)
    // 从这个校验的过程就知道,这个乐观锁完全不可靠,当校验成功后,在执行计算的之前,共享变量被改过了
    if (!sl.validate(stamp)) {
       stamp = sl.readLock();
       try {
         currentX = x;
         currentY = y;
        } finally {
          sl.unlockRead(stamp);
       }
    }
    // 这句执行之前,X、Y可能被改了
    return Math.sqrt(currentX * currentX + currentY * currentY);
 }
}
	public long tryOptimisticRead() {
        long s;
        return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
    }
    // 校验版本戳是否改过
	public boolean validate(long stamp) {
        U.loadFence();
        return (stamp & SBITS) == (state & SBITS);
    }

官方文档中表明说乐观锁可减少锁竞争,增加并发的吞吐量,从代码层面确实如此,但是从实际应用中,我觉得这种乐观锁是不够可靠的加锁,使用时要特别注意。

二、API特点

优点

  1. 有乐观读锁。减少了并发冲突,增大了并发吞吐量,也可以避免写锁饥饿。
  2. 读写可以相互转换。这减少了代码的开发量,不用先释放占有的锁,然后再去获取另外一种锁。

缺点

  1. 乐观读是不可靠的。乐观锁严格意义不是锁,只是用版本戳来判断共享资源是否被改过。
  2. 不能重入,重入会造成死锁。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值