摘要:一起来聊聊这个在高并发环境下比ReadWriteLock更快的锁——StampedLock 。本文分享自华为云社区《【高并发】一文彻底理解并发编程中非常重要的票据锁——StampedLock》,作者: 冰 河。
什么是StampedLock?ReadWriteLock锁允许多个线程同时读取共享变量,但是在读取共享变量的时候,不允许另外的线程多共享变量进行写操作,更多的适合于读多写少的环境中 。那么,在读多写少的环境中 , 有没有一种比ReadWriteLock更快的锁呢?
答案当然是有!那就是我们今天要介绍的主角——JDK1.8中新增的StampedLock!没错,就是它!
StampedLock与ReadWriteLock相比,在读的过程中也允许后面的一个线程获取写锁对共享变量进行写操作,为了避免读取的数据不一致,使用StampedLock读取共享变量时,需要对共享变量进行是否有写入的检验操作,并且这种读是一种乐观读 。
总之,StampedLock是一种在读取共享变量的过程中,允许后面的一个线程获取写锁对共享变量进行写操作,使用乐观读避免数据不一致的问题,并且在读多写少的高并发环境下,比ReadWriteLock更快的一种锁 。
StampedLock三种锁模式这里,我们可以简单对比下StampedLock与ReadWriteLock,ReadWriteLock支持两种锁模式:一种是读锁,另一种是写锁,并且ReadWriteLock允许多个线程同时读共享变量,在读时,不允许写 , 在写时 , 不允许读 , 读和写是互斥的,所以,ReadWriteLock中的读锁,更多的是指悲观读锁 。
StampedLock支持三种锁模式:写锁、读锁(这里的读锁指的是悲观读锁)和乐观读(很多资料和书籍写的是乐观读锁,这里我个人觉得更准确的是乐观读,为啥呢?我们继续往下看?。?。其中,写锁和读锁与ReadWriteLock中的语义类似,允许多个线程同时获取读锁,但是只允许一个线程获取写锁,写锁和读锁也是互斥的 。
另一个与ReadWriteLock不同的地方在于:StampedLock在获取读锁或者写锁成功后,都会返回一个Long类型的变量,之后在释放锁时,需要传入这个Long类型的变量 。例如,下面的伪代码所示的逻辑演示了StampedLock如何获取锁和释放锁 。
public class StampedLockDemo{ //创建StampedLock锁对象 public StampedLock stampedLock = new StampedLock(); //获取、释放读锁 public void testGetAndReleaseReadLock(){ long stamp = stampedLock.readLock(); try{ //执行获取读锁后的业务逻辑 }finally{ //释放锁 stampedLock.unlockRead(stamp); } } //获取、释放写锁 public void testGetAndReleaseWriteLock(){ long stamp = stampedLock.writeLock(); try{ //执行获取写锁后的业务逻辑 。}finally{ //释放锁 stampedLock.unlockWrite(stamp); } }}StampedLock支持乐观读,这是它比ReadWriteLock性能要好的关键所在 。 ReadWriteLock在读取共享变量时,所有对共享变量的写操作都会被阻塞 。而StampedLock提供的乐观读,在多个线程读取共享变量时,允许一个线程对共享变量进行写操作 。
我们再来看一下JDK官方给出的StampedLock示例 , 如下所示 。
class Point { private double x, y; private final StampedLock sl = new StampedLock(); void move(double deltaX, double deltaY) { // an exclusively locked method long stamp = sl.writeLock(); try {x += deltaX;y += deltaY; } finally { sl.unlockWrite(stamp); } } double distanceFromOrigin() { // A read-only method 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); } } return Math.sqrt(currentX * currentX + currentY * currentY); } void moveIfAtOrigin(double newX, double newY) { // upgrade // Could instead start with optimistic, not read mode long stamp = sl.readLock(); try { while (x == 0.0 && y == 0.0) { long ws = sl.tryConvertToWriteLock(stamp); if (ws != 0L) {stamp = ws;x = newX;y = newY; break; } else { sl.unlockRead(stamp);stamp = sl.writeLock(); } } } finally { sl.unlock(stamp); } }}在上述代码中 , 如果在执行乐观读操作时,另外的线程对共享变量进行了写操作,则会把乐观读升级为悲观读锁 , 如下代码片段所示 。
double distanceFromOrigin() { // A read-only method //乐观读 long stamp = sl.tryOptimisticRead(); double currentX = x, currentY = y; //判断是否有线程对变量进行了写操作 //如果有线程对共享变量进行了写操作 //则sl.validate(stamp)会返回false if (!sl.validate(stamp)) { //将乐观读升级为悲观读锁stamp = sl.readLock(); try { currentX = x; currentY = y; } finally { //释放悲观锁 sl.unlockRead(stamp); } } return Math.sqrt(currentX * currentX + currentY * currentY);}这种将乐观读升级为悲观读锁的方式相比一直使用乐观读的方式更加合理 , 如果不升级为悲观读锁,则程序会在一个循环中反复执行乐观读操作,直到乐观读操作期间没有线程执行写操作 , 而在循环中不断的执行乐观读会消耗大量的CPU资源,升级为悲观读锁是更加合理的一种方式 。
推荐阅读
- .NET 7 RC 2 发布,倒计时一个月发布正式版
- Java 最长公共前缀
- 王者荣耀金秋印记一个如何获取
- .NET平台下一个你不知道的框架,我只想说两个字:“牛逼”
- Go Micro介绍与入门
- 快手直播怎么只让一个人看(快手在哪里看直播回放)
- 阿姆斯特丹机场u002F史基浦机场 退税攻略 阿姆斯特丹是中国到欧洲的一个重要的转乘站
- 完 golang开发:go并发的建议
- 笔仙怎么玩……一个人又该怎么玩(笔仙的步骤)
- 一个对讲机可以怎么玩(对讲机一个人怎么玩)