JDK8中新增的StampedLock有什么作用


本篇内容主要讲解“JDK8中新增的StampedLock有什么作用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“JDK8中新增的StampedLock有什么作用”吧!StampedLock类,在JDK1.8时引入,是对读写锁ReentrantReadWriteLock的增强,该类提供了一些功能,优化了读锁、写锁的访问,同时使读写锁之间可以互相转换,更细粒度控制并发。首先明确下,该类的设计初衷是作为一个内部工具类,用于辅助开发其它线程安全组件,用得好,该类可以提升系统性能,用不好,容易产生死锁和其它莫名其妙的问题。上一篇文章,讲解了读写锁——ReentrantReadWriteLock原理详解 ,那么为什么有了ReentrantReadWriteLock,还要引入StampedLock?ReentrantReadWriteLock使得多个读线程同时持有读锁(只要写锁未被占用),而写锁是独占的。但是,读写锁如果使用不当,很容易产生“饥饿”问题:比如在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的。try系列获取锁的函数,当获取锁失败后会返回为0的stamp值。当调用释放锁和转换锁的方法时候需要传入获取锁时候返回的stamp值。StampedLockd的内部实现是基于CLH锁的,CLH锁原理:锁维护着一个等待线程队列,所有申请锁且失败的线程都记录在队列。一个节点代表一个线程,保存着一个标记位locked,用以判断当前线程是否已经释放锁。当一个线程试图获取锁时,从队列尾节点作为前序节点,循环判断所有的前序节点是否已经成功释放锁。先来看一个Oracle官方的例子:可以看到,上述示例最特殊的其实是distanceFromOrigin方法,这个方法中使用了“Optimistic reading”乐观读锁,使得读写可以并发执行,但是“Optimistic reading”的使用必须遵循以下模式:StampedLock虽然不像其它锁一样定义了内部类来实现AQS框架,但是StampedLock的基本实现思路还是利用CLH队列进行线程的管理,通过同步状态值来表示锁的状态和类型。StampedLock内部定义了很多常量,定义这些常量的根本目的还是和ReentrantReadWriteLock一样,对同步状态值按位切分,以通过位运算对State进行操作:对于StampedLock来说,写锁被占用的标志是第8位为1,读锁使用0-7位,正常情况下读锁数目为1-126,超过126时,使用一个名为readerOverflow的int整型保存超出数。另外,StampedLock相比ReentrantReadWriteLock,对多核CPU进行了优化,可以看到,当CPU核数超过1时,会有一些自旋操作:假设现在有多个线程:ThreadA、ThreadB、ThreadC、ThreadD、ThreadE。操作如下:
ThreadA调用writeLock————获取写锁
ThreadB调用readLock————获取读锁
ThreadC调用readLock————获取读锁
ThreadD调用writeLock————获取写锁
ThreadE调用readLock————获取读锁StampedLock的构造器很简单,构造时设置下同步状态值:另外,StamedLock提供了三类视图:这些视图其实是对StamedLock方法的封装,便于习惯了ReentrantReadWriteLock的用户使用:
例如,ReadLockView其实相当于ReentrantReadWriteLock.readLock()返回的读锁;来看下writeLock方法:说明:上述代码获取写锁,如果获取失败,则进入阻塞,注意该方法不响应中断,返回非0表示获取成功。StampedLock中大量运用了位运算,这里(s = state) & ABITS == 0L表示读锁和写锁都未被使用,这里写锁可以立即获取成功,然后CAS操作更新同步状态值State。操作完成后,等待队列的结构如下:注意:StampedLock中,等待队列的结点要比AQS中简单些,仅仅三种状态。
0:初始状态
-1:等待中
1:取消另外,结点的定义中有个cowait字段,该字段指向一个栈,用于保存读线程,这个后续会讲到。来看下readLock方法:
由于ThreadA此时持有写锁,所以ThreadB获取读锁失败,将调用acquireRead方法,加入等待队列:说明:上述代码获取读锁,如果写锁被占用,线程会阻塞,注意该方法不响应中断,返回非0表示获取成功。acquireRead方法非常复杂,用到了大量自旋操作:我们来分析下这个方法。
该方法会首先自旋的尝试获取读锁,获取成功后,就直接返回;否则,会将当前线程包装成一个读结点,插入到等待队列。
由于,目前等待队列还是空,所以ThreadB会初始化队列,然后将自身包装成一个读结点,插入队尾,然后在下面这个地方跳出自旋:此时,等待队列的结构如下:跳出自旋后,ThreadB会继续向下执行,进入下一个自旋,在下一个自旋中,依然会再次尝试获取读锁,如果这次再获取不到,就会将前驱的等待状态置为WAITING, 表示我(当前线程)要去睡了(阻塞),到时记得叫醒我:最终, ThreadB进入阻塞状态:最终,等待队列的结构如下:这个过程和ThreadB获取读锁一样,区别在于ThreadC被包装成结点加入等待队列后,是链接到ThreadB结点的栈指针中的。调用完下面这段代码后,ThreadC会链接到以Thread B为栈顶指针的栈中:说明:上述代码队列不为空,且队尾是读结点,则将添加当前结点链接到队尾结点的cowait链中(实际上构成一个栈,p是栈顶指针)注意:读结点的cowait字段其实构成了一个栈,入栈的过程其实是个“头插法”插入单链表的过程。比如,再来个ThreadX读结点,则cowait链表结构为:ThreadB - > ThreadX -> ThreadC。最终唤醒读结点时,将从栈顶开始。然后会在下一次自旋中,阻塞当前读线程:最终,等待队列的结构如下:可以看到,此时ThreadC结点并没有把它的前驱的等待状态置为-1,因为ThreadC是链接到栈中的,当写锁释放的时候,会从栈底 香港云主机元素开始,唤醒栈中所有读结点。ThreadD调用writeLock方法获取写锁失败后(ThreadA依然占用着写锁),会调用acquireWrite方法,该方法整体逻辑和acquireRead差不多,首先自旋的尝试获取写锁,获取成功后,就直接返回;否则,会将当前线程包装成一个写结点,插入到等待队列。说明:上述代码获取写锁,如果失败,则进入阻塞,注意该方法不响应中断,返回非0,表示获取成功acquireWrite源码:acquireWrite中的下面这个自旋操作,用于将线程包装成写结点,插入队尾:说明:上述代码自旋入队操作,如果没用任何锁被占用,则立即尝试获取写锁,获取成功则返回,如果存在锁被使用,则将当前线程包装成独占节点,并插入等待队列尾部插入完成后,队列结构如下:然后,进入下一个自旋,并在下一个自旋中阻塞ThreadD,最终队列结构如下:同样,由于写锁被ThreadA占用着,所以最终会调用acquireRead方法,在该方法的第一个自旋中,会将ThreadE加入等待队列:注意,由于队尾结点是写结点,所以当前读结点会直接链接到队尾;如果队尾是读结点,则会链接到队尾读结点的cowait链中。然后进入第二个自旋,阻塞ThreadE,最终队列结构如下:通过CAS操作,修改State成功后,会调用release方法唤醒等待队列的队首结点:release方法非常简单,先将头结点的等待状态置为0,表示即将唤醒后继结点,然后立即唤醒队首结点:此时,等待队列的结构如下:ThreadB被唤醒后,会从原阻塞处继续向下执行,然后开始下一次自旋:第二次自旋时,ThreadB发现写锁未被占用,则成功获取到读锁,然后从栈顶(ThreadB的cowait指针指向的结点)开始唤醒栈中所有线程,
最后返回:最终,等待队列的结构如下:ThreadC被唤醒后,继续执行,并进入下一次自旋,下一次自旋时,会成功获取到读锁。注意,此时ThreadB和ThreadC已经拿到了读锁,ThreadD(写线程)和ThreadE(读线程)依然阻塞中,原来ThreadC对应的结点是个孤立结点,会被GC回收。最终,等待队列的结构如下:ThreadB和ThreadC调用unlockRead方法释放读锁,CAS操作State将读锁数量减1:注意,当读锁的数量变为0时才会调用release方法,唤醒队首结点:队首结点(ThreadD写结点被唤醒),最终等待队列的结构如下:ThreadD会从原阻塞处继续向下执行,并在下一次自旋中获取到写锁,然后返回:最终,等待队列的结构如下:ThreadD释放写锁的过程和步骤7完全相同,会调用unlockWrite唤醒队首结点(ThreadE)。ThreadE被唤醒后会从原阻塞处继续向下执行,但由于ThreadE是个读结点,所以同时会唤醒cowait栈中的所有读结点,过程和步骤8完全一样。最终,等待队列的结构如下:至此,全部执行完成。StampedLock的等待队列与RRW的CLH队列相比,有以下特点:当入队一个线程时,如果队尾是读结点,不会直接链接到队尾,而是链接到该读结点的cowait链中,cowait链本质是一个栈;当入队一个线程时,如果队尾是写结点,则直接链接到队尾;QS类似唤醒线程的规则和A,都是首先唤醒队首结点。区别是StampedLock中,当唤醒的结点是读结点时,会唤醒该读结点的cowait链中的所有读结点(顺序和入栈顺序相反,也就是后进先出)。另外,StampedLock使用时要特别小心,避免锁重入的操作,在使用乐观读锁时也需要遵循相应的调用模板,防止出现数据不一致的问题。到此,相信大家对“JDK8中新增的StampedLock有什么作用”有了更深的了解,不妨来实际操作一番吧!这里是开发云网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

相关推荐: Windows中网页安全证书过期怎么办

小编给大家分享一下Windows中网页安全证书过期怎么办,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!1.按Win+R键,打开运行,输入“mmc”,点击确定。2.在控制台窗口中选择“…

免责声明:本站发布的图片视频文字,以转载和分享为主,文章观点不代表本站立场,本站不承担相关法律责任;如果涉及侵权请联系邮箱:360163164@qq.com举报,并提供相关证据,经查实将立刻删除涉嫌侵权内容。

Like (0)
Donate 微信扫一扫 微信扫一扫
Previous 07/30 16:33
Next 07/30 16:33

相关推荐