李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
55.StampedLock介绍
Leefs
2022-11-28 PM
559℃
0条
[TOC] ### 前言 `ReadWriteLock`适用于读多写少的场景,允许多个线程同时读取共享变量。但在读多写少的场景中,还有更快的技术方案。在`jdk8`以后,java提供了一个性能更优越的读写锁并发类`StampedLock`,该类的设计初衷是作为一个内部工具类,用于辅助开发其它线程安全组件,用得好,该类可以提升系统性能,用不好,容易产生死锁和其它莫名其妙的问题。本文主要和大家一起学习下`StampedLock`的功能和使用。 ### 一、StampedLock概述 `StampedLock` 是读写锁的实现,对比 `ReentrantReadWriteLock` 主要不同是该锁不允许重入,多了乐观读的功能,使用上会更加复杂一些,但是具有更好的性能表现。 `StampedLock` 的状态由版本和读写锁持有计数组成。 获取锁方法返回一个邮戳,表示和控制与锁状态相关的访问; 这些方法的“尝试”版本可能会返回特殊值 0 来表示获取锁失败。 锁释放和转换方法需要邮戳作为参数,如果它们与锁的状态不匹配则失败。 ### 二、StampedLock支持的三种锁模式 `ReadWriteLock`支持两种访问模式:读锁和写锁,而`StampedLock`支持三种访问模式:写锁、悲观读锁和乐观读。 其中写锁和悲观读锁的语义与`ReadWriteLock`中的写锁和读锁语义类似,允许多个线程同时获取悲观读锁,只允许一个线程获取写锁。与`ReadWriteLock`不同的是,`StampedLock`中的写锁和悲观读锁加锁成功之后,都会返回一个stamp标记,然后解锁的时候需要传入这个stamp。 **相关示例代码** ```java final StampedLock sl = new StampedLock(); // 获取/释放悲观读锁示意代码 long stamp = sl.readLock(); try { //省略业务相关代码 } finally { sl.unlockRead(stamp); } // 获取/释放写锁示意代码 long stamp = sl.writeLock(); try { //省略业务相关代码 } finally { sl.unlockWrite(stamp); } ``` `StampedLock`的性能之所以比`ReadWriteLock`好,其关键在于`StampedLock`支持**乐观读**。`ReadWriteLock`支持多个线程同时读,当多个线程同时读的时候,所有的写操作都会被阻塞。但是,`StampedLock`提供了乐观读,当有多个线程同时读共享变量允许一个线程获取写锁,也就是说不是所有写操作都会被阻塞。 需要注意,`StampedLock`提供的是“乐观读”而不是“乐观读锁”,这表示乐观读是无锁的,这也是其比`ReadWriteLock`读锁性能好的原因。 **乐观读的使用示例** ```java class Point{ private int x, y; final StampedLock sl = new StampedLock(); // 计算到原点的距离 double distanceFromOrigin() { long stamp = sl.tryOptimisticRead(); //乐观读 //读取全局变量存储到局部变量中 在读入的过程中,数据可能被修改 int curX = x; int curY = y; //判断进行读操作期间,是否存在写操作,如果存在,则sl.validate(stamp)返回false if(!sl.validate(stamp)) { stamp = sl.readLock(); //升级为悲观读锁 一切的写操作都会被阻塞 try { curX = x; curY = y; }finally { sl.unlockRead(stamp); //释放悲观读锁 } } return Math.sqrt(curX*curX + curY*curY); } } ``` 我们将共享变量x,y读入方法的局部变量中,因为`tryOptimisticRead()`是无锁的,所以,共享变量x和y读入方法局部变量时,x和y有可能被其他线程修改了。 因此,最后读完之后,还需要再次验证一下在读入过程中是否存在写操作,这个验证操作是通过调用`validate(stamp)`来实现的。 如果在执行乐观读操作期间,存在写操作,会把乐观读升级为悲观读锁。 如果不使用这种做法,那么就可能需要使用循环来执行反复读,直到执行乐观读操作的期间没有写操作,但是循环会浪费大量的CPU。 所以,升级为悲观读锁,代码简练且不易出错。 ### 三、StampedLock乐观读的理解 **数据库中的乐观锁**与`StampedLock`中的乐观读有着异曲同工之妙。 **通过下面这个例子来理解**: 在ERP的生产模块中,会有多个人通过ERP系统提供的UI同时修改同一条生产订单,那如何保证生产订单数据是并发安全的? **一种解决方案是采用乐观锁。** 在生产订单的表`product_doc`里面增加了一个数据型版本号字段`vresion`,**每次更新product_doc这个表的时候,都将version字段加1**。生产订单的UI在展示的时候,需要查询数据库,此时将这个version字段和其他业务字段一起返回给生产订单UI。 假设用户查询的生产订单的id=777,那么SQL语句类似如下: ```sql select id, ..., version from product_doc where id=777 ``` 用户在生产订单UI执行保存操作的时候,后台利用下面的SQL语句更新生产订单,此处我们假设该条生产订单的version=4: ```sql update product_doc set version=version+1,... where id=777 and version=4 ``` 如果这条SQL语句执行成功并且返回条数等于1,那么说明从生产订单UI执行查询操作到执行保存期间,没有其他人修改过这条数据。因为如果这期间有人修改过这条数据,那么版本号字段一定会大于4。 数据库中的乐观锁,查询的时候,需要把version字段查出来,**更新的时候要利用version字段做验证**。`StampedLock`里面的stamp就类似于这个version字段。 ### 四、StampedLock相关方法 #### 4.1 写模式 获取写锁,它是独占的,当锁处于写模式时,无法获得读锁,所有乐观读验证都将失败。 | 方法 | 说明 | | --------------------------------------------- | ------------------------------------------------------------ | | `writeLock()` | 阻塞等待独占获取锁,返回一个戳, 如果是0表示获取失败 | | `tryWriteLock()` | 尝试获取一个写锁,返回一个戳, 如果是0表示获取失败 | | `long tryWriteLock(long time, TimeUnit unit)` | 尝试获取一个独占写锁,可以等待一段事件,返回一个戳, 如果是0表示获取失败 | | `long writeLockInterruptibly()` | 试获取一个独占写锁,可以被中断,返回一个戳, 如果是0表示获取失败 | | `unlockWrite(long stamp)` | 释放独占写锁,传入之前获取的戳 | | `tryUnlockWrite()` | 如果持有写锁,则释放该锁,而不需要戳值。这种方法可能对错误后的恢复很有用 | **语法** ```java long stamp = lock.writeLock(); try { .... } finally { lock.unlockWrite(stamp); } ``` #### 4.2 读模式 悲观的方式后去非独占读锁。 | 方法 | 说明 | | -------------------------------------------- | ------------------------------------------------------------ | | `readLock()` | 阻塞等待获取非独占的读锁,返回一个戳, 如果是0表示获取失败 | | `tryReadLock()` | 尝试获取一个读锁,返回一个戳, 如果是0表示获取失败 | | `long tryReadLock(long time, TimeUnit unit)` | 尝试获取一个读锁,可以等待一段事件,返回一个戳, 如果是0表示获取失败 | | `long readLockInterruptibly()` | 阻塞等待获取非独占的读锁,可以被中断,返回一个戳, 如果是0表示获取失败 | | `unlockRead(long stamp)` | 释放非独占的读锁,传入之前获取的戳 | | `tryUnlockRead()` | 如果读锁被持有,则释放一次持有,而不需要戳值。这种方法可能对错误后的恢复很有用 | **语法** ```java long stamp = lock.readLock(); try { .... } finally { lock.unlockRead(stamp); } ``` #### 4.3 乐观读模式 `StampedLock `支持 `tryOptimisticRead() `方法,读取完毕后做一次戳校验,如果校验通过,表示这期间没有其他线程的写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据一致性。 | 方法 | 说明 | | ------------------------------ | ---------------------------------------------------- | | `tryOptimisticRead()` | 返回稍后可以验证的戳记,如果独占锁定则返回零 | | `boolean validate(long stamp)` | 如果自给定戳记发行以来锁还没有被独占获取,则返回true | **语法** ```java long stamp = lock.tryOptimisticRead(); // 验戳 if(!lock.validate(stamp)){ // 锁升级 } ``` 此外,StampedLock 提供了api实现下面3种方式进行转换: + **long tryConvertToWriteLock(long stamp)** 验证当前锁版本和锁持有状态和给定的邮戳是否匹配,如果不匹配、邮戳的锁状态有误或当前持有多个共享锁则返回 0。匹配时则分三种情况,当前未持有锁则获取独占锁,当前持有独占锁则不进行操作,当前仅持有一个共享锁则释放共享锁获取独占锁,最终返回独占锁的邮戳。 + **long tryConvertToReadLock(long stamp)** 验证当前锁版本和锁持有状态和给定的邮戳是否匹配,如果不匹配或者邮戳的锁状态有误则返回 0。匹配时则分三种情况,当前未持有锁则获取共享锁,当前持有独占锁则释放独占锁获取共享锁,当前持有共享锁则不进行操作,最终返回共享锁的邮戳。 + **long tryConvertToOptimisticRead(long stamp)** 验证当前锁版本和锁持有状态和给定的邮戳是否匹配,如果匹配则进行一次锁释放,如果不匹配或者邮戳的锁状态有误则返回 0。该方法的逻辑和 `unlock` 方法的逻辑相似,如果当前未持有锁就直接返回锁版本,如果持有锁则进行一次锁释放,再返回锁版本。 ### 五、读写案示例 提供一个数据容器类内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法 ```java @Slf4j(topic = "c.DataContainerStamped") class DataContainerStamped { //数据 private int data; //StampedLock 锁 private final StampedLock lock = new StampedLock(); public DataContainerStamped(int data) { this.data = data; } //读取操作 public int read(int readTime) { //首先获取stamp long stamp = lock.tryOptimisticRead(); log.debug("optimistic read locking...{}", stamp); sleep(readTime); //验证如果是有效的,证明这期间没有写操作,直接返回即可,这时还是乐观锁 if (lock.validate(stamp)) { //就可以读到数据 log.debug("read finish...{}, data:{}", stamp, data); return data; } // 否则证明已经有写锁修改过了,这里需要再次获取读锁,升级为真正的读锁 // 锁升级 - 读锁 log.debug("updating to read lock... {}", stamp); try { //获取stamp stamp = lock.readLock(); log.debug("read lock {}", stamp); sleep(readTime); log.debug("read finish...{}, data:{}", stamp, data); return data; } finally { log.debug("read unlock {}", stamp); lock.unlockRead(stamp); } } public void write(int newData) { //获取戳 long stamp = lock.writeLock(); log.debug("write lock {}", stamp); try { sleep(2); this.data = newData; } finally { log.debug("write unlock {}", stamp); lock.unlockWrite(stamp); } } } ``` #### 测试 `读-读` 可以优化 ```java public static void main(String[] args) { DataContainerStamped dataContainer = new DataContainerStamped(1); new Thread(() -> { dataContainer.read(1); }, "t1").start(); sleep(0.5); new Thread(() -> { dataContainer.read(0); }, "t2").start(); } ``` **运行结果** ``` 22:16:39.577 [t1] DEBUG c.DataContainerStamped - optimistic read locking...256 22:16:40.082 [t2] DEBUG c.DataContainerStamped - optimistic read locking...256 22:16:40.082 [t2] DEBUG c.DataContainerStamped - read finish...256, data:1 22:16:40.582 [t1] DEBUG c.DataContainerStamped - read finish...256, data:1 ``` **结果分析** 从结果中可以看到两个线程同时获取读锁并执行读操作,没有先后的关系。 #### 测试 `读-写` 时优化读补加读锁 ```java public static void main(String[] args) { DataContainerStamped dataContainer = new DataContainerStamped(1); new Thread(() -> { dataContainer.read(1); }, "t1").start(); sleep(0.5); new Thread(() -> { dataContainer.write(0); }, "t2").start(); } ``` **运行结果** ``` 22:18:14.893 [t1] DEBUG c.DataContainerStamped - optimistic read locking...256 22:18:15.392 [t2] DEBUG c.DataContainerStamped - write lock 384 22:18:15.899 [t1] DEBUG c.DataContainerStamped - updating to read lock... 256 22:18:17.393 [t2] DEBUG c.DataContainerStamped - write unlock 384 22:18:17.393 [t1] DEBUG c.DataContainerStamped - read lock 513 22:18:18.393 [t1] DEBUG c.DataContainerStamped - read finish...513, data:0 22:18:18.393 [t1] DEBUG c.DataContainerStamped - read unlock 513 ``` **结果分析** 一开始是读操作先睡眠一秒,在睡眠之前已经获取了戳了,在 t1 线程睡眠期间 t2 线程获取到了写锁,并将数据修改,而且戳也改成了384。 此时 t1 线程醒过来校验发现戳已经被修改了,所以这时候 t1 线程会等待 t2 线程释放写锁之后去获取读锁。完成从`乐观读 -> 读锁` 的升级。 > **注意** > > + `StampedLock` 不支持条件变量 > + `StampedLock` 不支持可重入 *附参考文章链接* *https://jiuaidu.com/jianzhan/966945/*
标签:
并发编程
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://www.lilinchao.com/archives/2651.html
上一篇
54.ReentrantReadWriteLock实现原理详解
下一篇
56.Semaphore介绍
取消回复
评论啦~
提交评论
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
标签云
ClickHouse
gorm
JavaWEB项目搭建
Quartz
持有对象
Java编程思想
数据结构和算法
Redis
正则表达式
Livy
锁
Linux
Jquery
队列
Netty
Shiro
GET和POST
JavaScript
Java阻塞队列
Flume
栈
DataX
nginx
Zookeeper
国产数据库改造
Docker
Nacos
人工智能
Beego
递归
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞