李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
55.StampedLock介绍
Leefs
2022-11-28 PM
965℃
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://lilinchao.com/archives/2651.html
上一篇
54.ReentrantReadWriteLock实现原理详解
下一篇
56.Semaphore介绍
评论已关闭
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
NLP
4
标签云
队列
JVM
JavaScript
Azkaban
Thymeleaf
Nacos
ajax
Kafka
递归
Redis
Map
线程池
Sentinel
FastDFS
Flink
查找
Hadoop
Spark SQL
DataX
GET和POST
Java工具类
gorm
国产数据库改造
Golang基础
Java
MyBatis
前端
Python
数据结构
序列化和反序列化
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞
评论已关闭