「原创」Java并发编程系列19 | JDK8新增锁StampedLock
mhr18 2024-10-09 12:23 43 浏览 0 评论
20大进阶架构专题每日送达
StampedLock
是 JDK1.8 版本中在 J.U.C 并发包里新增的一个锁,StampedLock
是对读写锁ReentrantReadWriteLock
的增强,优化了读锁、写锁的访问,更细粒度控制并发。这篇文章就来介绍一下StampedLock
,分为如下几个问题:
JDK1.8 为什么引入
StampedLock
StampedLock
锁的三种模式StampedLock
的使用以及注意问题
1. 为什么引入StampedLock
ReentrantReadWriteLock
的问题
既然说StampedLock
是对读写锁ReentrantReadWriteLock
的增强与优化,那么就要先弄清楚ReentrantReadWriteLock
到底存在什么问题。
ReentrantReadWriteLock
可能会导致写线程饥饿。关于并发编程中的公平与饥饿这里不再介绍了,不了解的可以看这篇公平锁与非公平锁。
首先我们来回顾读写锁的几个知识点:
读写锁多应用在读多写少的场景
读锁是共享锁,当一个线程持有读锁时其他线程是可以获取到读锁的
读写锁不支持锁升级,当一个线程持有读锁时,该线程自己和其他线程都是不可以获取写锁的
现在来解释下导致写线程饥饿的情况:当线程 A 持有读锁读取数据时,线程 B 要获取写锁修改数据就只能到队列里排队。此时又来了线程 C 读取数据,那么线程 C 就可以获取到读锁,而要执行写操作线程 B 就要等线程 C 释放读锁。由于该场景下读操作远远大于写的操作,此时可能会有很多线程来读取数据而获取到读锁,那么要获取写锁的线程 B 就只能一直等待下去,最终导致饥饿。
StampedLock
读写锁导致写线程饥饿的原因是读锁和写锁互斥,StampedLock
提供了解决这一问题的方案————乐观读锁 Optimistic reading,即一个线程获取的乐观读锁之后,不会阻塞线程获取写锁。
2. 三种锁模式
StampedLock
提供了三种模式来控制读写操作:写锁writeLock
、悲观读锁readLock
、乐观读锁Optimistic reading
。
写锁 writeLock
类似ReentrantReadWriteLock
的写锁,独占锁,当一个线程获取该锁后,其它请求的线程必须等待。
获取:没有线程持有悲观读锁或者写锁的时候才可以获取到该锁。
释放:请求该锁成功后会返回一个 stamp
票据变量用来表示该锁的版本,当释放该锁时候需要将这个stamp
作为参数传入解锁方法。
悲观读锁 readLock
类似ReentrantReadWriteLock
的读锁,共享锁,同时多个线程可以获取该锁。
获取:在没有线程获取独占写锁的情况下,同时多个线程可以获取该锁。
释放:请求该锁成功后会返回一个 stamp
票据变量用来表示该锁的版本,当释放该锁时候需要unlockRead
并传递参数stamp
。
悲观读锁:悲观的认为在具体操作数据前其他线程会对自己操作的数据进行修改,所以当前线程获取到悲观读锁的之后会阻塞线程获取写锁。
乐观读锁 tryOptimisticRead
获取:不需要通过 CAS 设置锁的状态,如果当前没有线程持有写锁,直接简单的返回一个非 0 的 stamp 版本信息,表示获取锁成功。
释放:并没有使用 CAS 设置锁状态所以不需要显示的释放该锁。
乐观读锁如何保证数据一致性呢?
乐观读锁在获取 stamp 时,会将需要的数据拷贝一份出来。在真正进行读取操作时,验证 stamp 是否可用。如何验证 stamp 是否可用呢?从获取 stamp 到真正进行读取操作这段时间内,如果有线程获取了写锁,stamp 就失效了。如果 stamp 可用就可以直接读取原来拷贝出来的数据,如果 stamp 不可用,就重新拷贝一份出来用。我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。
乐观读锁:乐观的认为在具体操作数据前其他线程不会对自己操作的数据进行修改,所以当前线程获取到乐观读锁的之后不会阻塞线程获取写锁。
为了保证数据一致性,在具体操作数据前要检查一下自己操作的数据是否经过修改操作了,如果进行了修改操作,就重新读一次。
乐观读锁在读多写少的情况下提供更好的性能,因为乐观读锁不需要进行 CAS 设置锁的状态而只是简单的测试状态。
3. 使用详解
Oracle 官方的例子:
class Point {
private double x, y;// 成员变量
private final StampedLock sl = new StampedLock;// 锁实例
/**
* 写锁writeLock
* 添加增量,改变当前point坐标的位置。
* 先获取到了写锁,然后对point坐标进行修改,然后释放锁。
* 写锁writeLock是排它锁,保证了其他线程调用move函数时候会被阻塞,直到当前线程显示释放了该锁,也就是保证了对变量x,y操作的原子性。
*/
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock;
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
/**
* 乐观读锁tryOptimisticRead
* 计算当前位置到原点的距离
*/
double distanceFromOrigin {
long stamp = sl.tryOptimisticRead; // 尝试获取乐观读锁(1)
double currentX = x, currentY = y; // 将全部变量拷贝到方法体栈内(2)
// 检查票据是否可用,即写锁有没有被占用(3)
if (!sl.validate(stamp)) {
// 如果写锁被抢占,即数据进行了写操作,则重新获取
stamp = sl.readLock;// 获取悲观读锁(4)
try {
// 将全部变量拷贝到方法体栈内(5)
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);// 释放悲观读锁(6)
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);// 真正读取操作,返回计算结果(7)
}
/**
* 悲观读锁readLock
* 如果当前坐标为原点则移动到指定的位置
*/
void moveIfAtOrigin(double newX, double newY) {
long stamp = sl.readLock;// 获取悲观读锁(1)
try {
// 如果当前点在原点则移动(2)
while (x == 0.0 && y == 0.0) {
long ws = sl.tryConvertToWriteLock(stamp);// 尝试将获取的读锁升级为写锁(3)
if (ws != 0L) {
// 升级成功,则更新票据,并设置坐标值,然后退出循环(4)
stamp = ws;
x = newX;
y = newY;
break;
} else {
// 读锁升级写锁失败,则释放读锁,显示获取独占写锁,然后循环重试(5)
sl.unlockRead(stamp);
stamp = sl.writeLock;
}
}
} finally {
sl.unlock(stamp);// 释放写锁(6)
}
}
}
写锁 writeLock
move
方法,添加增量,改变当前 point 坐标的位置。没啥可说的,就是正常的独占锁。
乐观读锁 Optimistic reading
distanceFromOrigin
方法,计算当前位置到原点的距离。
代码(1)首先尝试获取乐观读锁,如果当前没有其它线程获取到了写锁,那么(1)会返回一个非 0 的 stamp 用来表示版本信息。如果当前有线程占有写锁,返回的 stamp 为 0,会在代码(3)中检验失败。这里获取乐观锁并没有通过 CAS 操作修改锁的状态而是简单的通过与或操作返回了一个版本信息。
代码(2)拷贝变量到本地方法栈里面。
代码(3)检查在(1)获取到的票据 stamp 是否还有效,从执行完代码(1)到执行代码(3)这段时间内,如果有线程获取了写锁,stamp 就失效了。之所以还要在此校验是因为代码(1)获取读锁时候并没有通过 CAS 操作修改锁的状态而是简单的通过与或操作返回了一个版本信息。这里如果校验成功则执行(7)使用本地方法栈里面的值进行计算然后返回,也就是真正的读操作。需要注意的是在代码(3)校验成功后,代码(7)计算中其他线程可能获取到了写锁并且修改了 x,y 的值,而当前线程执行代码(7)进行计算时候采用的是修改前值的拷贝,也就是说操作是对之前值的一个拷贝,并不是新的值。
代码(2)和(3)能否互换呢?不能。假设位置换了,那么首先执行 validate,假如验证通过了,要拷贝 x,y 值到本地方法栈,而在拷贝的过程中很有可能其他线程已经修改了 x,y 中的一个,这就造成了数据的不一致性了。而不交换(2)和(3),如果在拷贝 x,y 值到本地方法栈里面时候也会存在其他线程修改了 x,y 中的一个值,那么肯定是有线程获取写锁进行了修改,validate 校验时候就会失败。
代码(4)在 validate 检验失败后获取悲观读锁,如果此时有线程持有写锁则代码(4)会导致的当前线程阻塞直到其它线程释放了写锁。写锁释放,也就是修改完成后唤醒当前线程执行下面的拷贝操作。
代码(5)获取到读锁后,拷贝变量到本地方法栈。
代码(6)释放悲观读锁,拷贝的时候由于加了读锁保证了在拷贝期间其它线程不能获取写锁来修改数据,从而保证了数据的一致性。
代码(7)使用方法栈里面数据计算返回,这里在计算时候使用的数据也可能不是最新的,其它写线程可能已经修改过原来的 x,y 值了。
总结乐观读锁的使用步骤:
long stamp = lock.tryOptimisticRead; // 非阻塞获取版本信息
copyVaraibale2ThreadMemory; // 拷贝变量到线程本地堆栈
if(!lock.validate(stamp)){ // 校验
long stamp = lock.readLock; // 获取读锁
try {
copyVaraibale2ThreadMemory; // 拷贝变量到线程本地堆栈
} finally {
lock.unlock(stamp); // 释放悲观锁
}
}
useThreadMemoryVarables; // 使用线程本地堆栈里面的数据进行操作
悲观读锁 readLock
moveIfAtOrigin
方法,如果当前坐标为原点则移动到指定的位置。
代码(1)获取悲观读锁,保证其它线程不能获取写锁修改 x,y 值。
代码(2)判断当前点在原点则更新坐标
代码(3)尝试升级读锁为写锁,这里升级不一定成功,因为多个线程都可以同时获取悲观读锁,当多个线程都执行到(3)时候只有一个可以升级成功,升级成功则返回非 0 的 stamp,否非返回 0。
假设当前线程升级成功,然后执行步骤(4)更新 stamp 值和坐标值然后退出循环;如果升级失败则执行步骤(5)首先释放读锁然后申请写锁,获取到写锁后在循环重新设置坐标值。
4. 注意问题
StampedLock
是不可重入的,如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁。StampedLock
支持读锁和写锁的相互转换。我们知道ReentrantReadWriteLock
中,当线程获取到写锁后,可以降级为读锁,但是读锁是不能直接升级为写锁的。而StampedLock
提供了读锁和写锁相互转换的功能,使得该类支持更多的应用场景。
总结
读写锁在读线程非常多,写线程很少的情况下可能会导致写线程饥饿,JDK1.8 新增的StampedLock
通过乐观读锁来解决这一问题。
StampedLock
有三种访问模式:
①写锁writeLock:功能和读写锁的写锁类似
②悲观读锁readLock:功能和读写锁的读锁类似
③乐观读锁Optimistic reading:一种优化的读模式
所有获取锁的方法,都返回一个票据 Stamp,Stamp 为 0 表示获取失败,其余都表示成功;所有释放锁的方法,都需要一个票据 Stamp,这个 Stamp 必须是和成功获取锁时得到的 Stamp 一致。
乐观读锁:乐观的认为在具体操作数据前其他线程不会对自己操作的数据进行修改,所以当前线程获取到乐观读锁的之后不会阻塞线程获取写锁。为了保证数据一致性,在具体操作数据前要检查一下自己操作的数据是否经过修改操作了,如果进行了修改操作,就重新读一次。因为乐观读锁不需要进行 CAS 设置锁的状态而只是简单的测试状态,所以在读多写少的情况下有更好的性能。
并发系列文章汇总
【原创】01|开篇获奖感言
【原创】02|并发编程三大核心问题
【原创】03|重排序-可见性和有序性问题根源
【原创】04|Java 内存模型详解
【原创】05|深入理解 volatile
【原创】06|你不知道的 final
【原创】07|synchronized 原理
【原创】08|synchronized 锁优化
【原创】09|基础干货
【原创】10|线程状态
【原创】11|线程调度
【原创】13|LockSupport
【原创】14|AQS 源码分析
【原创】15|重入锁 ReentrantLock
【原创】16|公平锁与非公平锁
【原创】17|读写锁八讲(上)
之前,给大家发过三份Java面试宝典,这次新增了一份,目前总共是四份面试宝典,相信在跳槽前一个月按照面试宝典准备准备,基本没大问题。
《java面试宝典5.0》(初中级)
《350道Java面试题:整理自100+公司》(中高级)
《资深java面试宝典-视频版》(资深)
《Java[BAT]面试必备》(资深)
分别适用于初中级,中高级,资深级工程师的面试复习。
内容包含java基础、javaweb、mysql性能优化、JVM、锁、百万并发、消息队列,高性能缓存、反射、Spring全家桶原理、微服务、Zookeeper、数据结构、限流熔断降级等等。
看到这里,证明有所收获
相关推荐
- Redis合集-使用benchmark性能测试
-
采用开源Redis的redis-benchmark工具进行压测,它是Redis官方的性能测试工具,可以有效地测试Redis服务的性能。本次测试使用Redis官方最新的代码进行编译,详情请参见Redis...
- Java简历总被已读不回?面试挂到怀疑人生?这几点你可能真没做好
-
最近看了几十份简历,发现大部分人不是技术差,而是不会“卖自己”——一、简历死穴:你写的不是经验,是岗位说明书!反面教材:ד使用SpringBoot开发项目”ד负责用户模块功能实现”救命写法:...
- redission YYDS(redission官网)
-
每天分享一个架构知识Redission是一个基于Redis的分布式Java锁框架,它提供了各种锁实现,包括可重入锁、公平锁、读写锁等。使用Redission可以方便地实现分布式锁。red...
- 从数据库行锁到分布式事务:电商库存防超卖的九重劫难与破局之道
-
2023年6月18日我们维护的电商平台在零点刚过3秒就遭遇了严重事故。监控大屏显示某爆款手机SKU_IPHONE13_PRO_MAX在库存仅剩500台时,订单系统却产生了1200笔有效订单。事故复盘发...
- SpringBoot系列——实战11:接口幂等性的形而上思...
-
欢迎关注、点赞、收藏。幂等性不仅是一种技术需求,更是数字文明对确定性追求的体现。在充满不确定性的网络世界中,它为我们建立起可依赖的存在秩序,这或许正是技术哲学最深刻的价值所在。幂等性的本质困境在支付系...
- 如何优化系统架构设计缓解流量压力提升并发性能?Java实战分享
-
如何优化系统架构设计缓解流量压力提升并发性能?Java实战分享在高流量场景下。首先,我需要回忆一下常见的优化策略,比如负载均衡、缓存、数据库优化、微服务拆分这些。不过,可能还需要考虑用户的具体情况,比...
- Java面试题: 项目开发中的有哪些成长?该如何回答
-
在Java面试中,当被问到“项目中的成长点”时,面试官不仅想了解你的技术能力,更希望看到你的问题解决能力、学习迭代意识以及对项目的深度思考。以下是回答的策略和示例,帮助你清晰、有说服力地展示成长点:一...
- 互联网大厂后端必看!Spring Boot 如何实现高并发抢券逻辑?
-
你有没有遇到过这样的情况?在电商大促时,系统上线了抢券活动,结果活动刚一开始,服务器就不堪重负,出现超卖、系统崩溃等问题。又或者用户疯狂点击抢券按钮,最后却被告知无券可抢,体验极差。作为互联网大厂的后...
- 每日一题 |10W QPS高并发限流方案设计(含真实代码)
-
面试场景还原面试官:“如果系统要承载10WQPS的高并发流量,你会如何设计限流方案?”你:“(稳住,我要从限流算法到分布式架构全盘分析)…”一、为什么需要限流?核心矛盾:系统资源(CPU/内存/数据...
- Java面试题:服务雪崩如何解决?90%人栽了
-
服务雪崩是指微服务架构中,由于某个服务出现故障,导致故障在服务之间不断传递和扩散,最终造成整个系统崩溃的现象。以下是一些解决服务雪崩问题的常见方法:限流限制请求速率:通过限流算法(如令牌桶算法、漏桶算...
- 面试题官:高并发经验有吗,并发量多少,如何回复?
-
一、有实际高并发经验(建议结构)直接量化"在XX项目中,系统日活用户约XX万,核心接口峰值QPS达到XX,TPS处理能力为XX/秒。通过压力测试验证过XX并发线程下的稳定性。"技术方案...
- 瞬时流量高并发“保命指南”:这样做系统稳如泰山,老板跪求加薪
-
“系统崩了,用户骂了,年终奖飞了!”——这是多少程序员在瞬时大流量下的真实噩梦?双11秒杀、春运抢票、直播带货……每秒百万请求的冲击,你的代码扛得住吗?2025年了,为什么你的系统一遇高并发就“躺平”...
- 其实很多Java工程师不是能力不够,是没找到展示自己的正确姿势。
-
其实很多Java工程师不是能力不够,是没找到展示自己的正确姿势。比如上周有个小伙伴找我,五年经验但简历全是'参与系统设计''优化接口性能'这种空话。我就问他:你做的秒杀...
- PHP技能评测(php等级考试)
-
公司出了一些自我评测的PHP题目,现将题目和答案记录于此,以方便记忆。1.魔术函数有哪些,分别在什么时候调用?__construct(),类的构造函数__destruct(),类的析构函数__cal...
- 你的简历在HR眼里是青铜还是王者?
-
你的简历在HR眼里是青铜还是王者?兄弟,简历投了100份没反应?面试总在第三轮被刷?别急着怀疑人生,你可能只是踩了这些"隐形求职雷"。帮3630+程序员改简历+面试指导和处理空窗期时间...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)