「原创」Java并发编程系列19 | JDK8新增锁StampedLock
mhr18 2024-10-09 12:23 59 浏览 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、数据结构、限流熔断降级等等。
看到这里,证明有所收获
相关推荐
- Java培训机构,你选对了吗?(java培训机构官网)
-
如今IT行业发展迅速,不仅是大学生,甚至有些在职的员工都想学习java开发,需求量的扩大,薪资必定增长,这也是更多人选择java开发的主要原因。不过对于没有基础的学员来说,java技术不是一两天就能...
- 产品经理MacBook软件清单-20个实用软件
-
三年前开始使用MacBookPro,从此再也不想用Windows电脑了,作为生产工具,MacBook可以说是非常胜任。作为产品经理,值得拥有一台MacBook。MacBook是工作平台,要发挥更大作...
- RAD Studio(Delphi) 本月隆重推出新的版本12.3
-
#在头条记录我的2025#自2024年9月,推出Delphi12.2版本后,本月隆重推出新的版本12.3,RADStudio12.3,包含了Delphi12.3和C++builder12.3最...
- 图解Java垃圾回收机制,写得非常好
-
什么是自动垃圾回收?自动垃圾回收是一种在堆内存中找出哪些对象在被使用,还有哪些对象没被使用,并且将后者删掉的机制。所谓使用中的对象(已引用对象),指的是程序中有指针指向的对象;而未使用中的对象(未引用...
- Centos7 初始化硬盘分区、挂载(针对2T以上)添加磁盘到卷
-
1、通过命令fdisk-l查看硬盘信息:#fdisk-l,发现硬盘为/dev/sdb大小4T。2、如果此硬盘以前有过分区,则先对磁盘格式化。命令:mkfs.文件系统格式-f/dev/sdb...
- 半虚拟化如何提高服务器性能(虚拟化 半虚拟化)
-
半虚拟化是一种重新编译客户机操作系统(OS)将其安装在虚拟机(VM)上的一种虚拟化类型,并在主机操作系统(OS)运行的管理程序上运行。与传统的完全虚拟化相比,半虚拟化可以减少开销,并提高系统性能。虚...
- HashMap底层实现原理以及线程安全实现
-
HashMap底层实现原理数据结构:HashMap的底层实现原理主要依赖于数组+链表+红黑树的结构。1、数组:HashMap最底层是一个数组,称为table,它存放着键值对。2、链...
- long和double类型操作的非原子性探究
-
前言“深入java虚拟机”中提到,int等不大于32位的基本类型的操作都是原子操作,但是某些jvm对long和double类型的操作并不是原子操作,这样就会造成错误数据的出现。其实这里的某些jvm是指...
- 数据库DELETE 语句,还保存原有的磁盘空间
-
MySQL和Oracle的DELETE语句与数据存储MySQL的DELETE操作当你在MySQL中执行DELETE语句时:逻辑删除:数据从表中标记为删除,不再可见于查询结果物理...
- 线程池—ThreadPoolExecutor详解(线程池实战)
-
一、ThreadPoolExecutor简介在juc-executors框架概述的章节中,我们已经简要介绍过ThreadPoolExecutor了,通过Executors工厂,用户可以创建自己需要的执...
- navicat如何使用orcale(详细步骤)
-
前言:看过我昨天文章的同鞋都知道最近接手另一个国企项目,数据库用的是orcale。实话实说,也有快三年没用过orcale数据库了。这期间问题不断,因为orcale日渐消沉,网上资料也是真真假假,难辨虚...
- 你的程序是不是慢吞吞?GraalVM来帮你飞起来性能提升秘籍大公开
-
各位IT圈内外的朋友们,大家好!我是你们的老朋友,头条上的IT技术博主。不知道你们有没有这样的经历:打开一个软件,半天没反应;点开一个网站,图片刷不出来;或者玩个游戏,卡顿得想砸电脑?是不是特别上火?...
- 大数据正当时,理解这几个术语很重要
-
目前,大数据的流行程度远超于我们的想象,无论是在云计算、物联网还是在人工智能领域都离不开大数据的支撑。那么大数据领域里有哪些基本概念或技术术语呢?今天我们就来聊聊那些避不开的大数据技术术语,梳理并...
- 秒懂列式数据库和行式数据库(列式数据库的特点)
-
行式数据库(Row-Based)数据按行存储,常见的行式数据库有Mysql,DB2,Oracle,Sql-server等;列数据库(Column-Based)数据存储方式按列存储,常见的列数据库有Hb...
- AMD发布ROCm 6.4更新:带来了多项底层改进,但仍不支持RDNA 4
-
AMD宣布,对ROCm软件栈进行了更新,推出了新的迭代版本ROCm6.4。这一新版本里,AMD带来了多项底层改进,包括更新改进了ROCm的用户空间库和AMDKFD内核驱动程序之间的兼容性,使其更容易...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (74)
- oracle批量插入数据 (65)
- oracle事务隔离级别 (59)
- oracle 空为0 (51)
- oracle主从同步 (56)
- oracle 乐观锁 (53)
- 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)