Java并发编程之StampedLock

Java并发编程之StampedLock

引言: StampedLock类是在JDK1.8时引入,是对读写锁ReentrantReadWriteLock的增强,该类提供了一些功能,优化了读锁、写锁的访问,同时使读写锁之间可以互相转换,更细粒度控制并发。ReentrantReadWriteLock使得多个读线程同时持有读锁(只要写锁未被占用),而写锁是独占的。但是,读写锁如果使用不当,很容易产生“饥饿”问题。比如在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的。

原理: 在StampedLock中使用了CLH自旋锁,它保证没有饥饿的发生,并且可以保证FIFO(先进先出)的服务顺序。在线程发生了读失败,不立刻把读线程挂起,锁当中维护了一个等待线程队列。所有申请锁但是没有成功的线程都会记录到这个队列中,每一个节点(一个节点表示一个线程)保存一个标记位(locked),用于判断当前线程是否已经释放锁。当一个未标记到队列中的线程试图获得锁时,会取得当前等待队列尾部的节点作为其前序节点,判断前序节点是否已经成功的释放了锁,只要前序节点(pred)没有释放锁,则表示当前线程还不能继续执行,因此会自旋等待, 反之,如果前序线程已经释放锁,则当前线程可以继续执行。释放锁时,也遵循这个逻辑,线程会将自身节点的locked位置标记位false,那么后续等待的线程就能继续执行了。

主要特点:
1、所有获取锁的方法,都会返回一个邮戳(Stamp),Stamp为0表示获取失败,其余都表示成功;
2、所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
3、StampedLock是不可重入的;(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
4、StampedLock有三种访问模式:
①Reading(读模式):悲观锁readLock,是个共享锁,在没有线程获取独占写锁的情况下,同时多个线程可以获取该锁;如果已经有线程持有写锁,其他线程请求获取该锁会被阻塞,这类似ReentrantReadWriteLock的读锁(不同在于这里的读锁是不可重入锁)。这里说的悲观是指在具体操作数据前,悲观的认为其他线程可能要对自己操作的数据进行修改,所以需要先对数据加锁,这是在读少写多的情况下的一种考虑,请求该锁成功后会返回一个stamp票据变量来表示该锁的版本,源码如下:

/**
     *  悲观读锁,非独占锁,为获得锁一直处于阻塞状态,直到获得锁为止
     */
    public long readLock() {
        long s = state, next;  
        // 队列为空   && 没有写锁同时读锁数小于126  && CAS修改状态成功      则状态加1并返回,否则自旋获取读锁
        return ((whead == wtail && (s & ABITS) < RFULL &&
                 U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
                next : acquireRead(false, 0L));
    }

②Writing(写模式):写锁writeLock是一个独占锁,同时只有一个线程可以获取该锁,当一个线程获取该锁后,其他请求读锁和写锁的线程必须等待,这跟ReentrantReadWriteLock的写锁很相似,不过要注意的是StampedLock的写锁是不可重入锁,当目前没有线程持有读锁或者写锁的时候才可以获取到该锁,请求该锁成功后会返回一个stamp 票据变量来表示该锁的版本,源码如下:

/**
     * 
     *获取写锁,获取失败会一直阻塞,直到获得锁成功
     * @return 可以用来解锁或转换模式的戳记(128的整数)
     */
    public long writeLock() {
        long s, next;  
        return ((((s = state) & ABITS) == 0L &&   // 完全没有任何锁(没有读锁和写锁)的时候可以通过
                 U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ? //第8位置为1
                next : acquireWrite(false, 0L));
    }

③OptimisticReading(乐观读模式):乐观读锁 tryoptimisticRead,这是一种优化的读模式,是相对于悲观锁来说的,在操作数据前并没有通过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;
    }

5、StampedLock支持读锁和写锁的相互转换,我们知道RRW中,当线程获取到写锁后,可以降级为读锁,但是读锁是不能直接升级为写锁的。StampedLock提供了读锁和写锁相互转换的功能,使得该类支持更多的应用场景。

总结:
1、相较于synchronized是在JVM层面上实现的,并且在代码执行时出现异常时JVM会自动释放锁定而言,StampedLock是对象层面的锁定,要保证锁定一定会被释放,就必须将unLock()放到finally()中;
2、StampedLock 对吞吐量有巨大的改进,特别是在读线程越来越多的场景下;

本文参考
本文主要参考以下文章,谨以技术分享为目的,将此文搬到CSDN上,如有侵权问题请联系本人,乐于分享提高。
作者: Ressmix
链接:https://segmentfault.com/a/1190000015808032?utm_source=tag-newest

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值