StampedLock介绍
前言
ReadWriteLock
适用于读多写少的场景,允许多个线程同时读取共享变量。但在读多写少的场景中,还有更快的技术方案。在jdk8
以后,java提供了一个性能更优越的读写锁并发类StampedLock
,该类的设计初衷是作为一个内部工具类,用于辅助开发其它线程安全组件,用得好,该类可以提升系统性能,用不好,容易产生死锁和其它莫名其妙的问题。本文主要和大家一起学习下StampedLock
的功能和使用。
一、StampedLock概述
StampedLock
是读写锁的实现,对比 ReentrantReadWriteLock
主要不同是该锁不允许重入,多了乐观读的功能,使用上会更加复杂一些,但是具有更好的性能表现。
StampedLock
的状态由版本和读写锁持有计数组成。 获取锁方法返回一个邮戳,表示和控制与锁状态相关的访问; 这些方法的“尝试”版本可能会返回特殊值 0 来表示获取锁失败。 锁释放和转换方法需要邮戳作为参数,如果它们与锁的状态不匹配则失败。
二、StampedLock支持的三种锁模式
ReadWriteLock
支持两种访问模式:读锁和写锁,而StampedLock
支持三种访问模式:写锁、悲观读锁和乐观读。
其中写锁和悲观读锁的语义与ReadWriteLock
中的写锁和读锁语义类似,允许多个线程同时获取悲观读锁,只允许一个线程获取写锁。与ReadWriteLock
不同的是,StampedLock
中的写锁和悲观读锁加锁成功之后,都会返回一个stamp标记,然后解锁的时候需要传入这个stamp。
相关示例代码
1 |
|
StampedLock
的性能之所以比ReadWriteLock
好,其关键在于StampedLock
支持乐观读。ReadWriteLock
支持多个线程同时读,当多个线程同时读的时候,所有的写操作都会被阻塞。但是,StampedLock
提供了乐观读,当有多个线程同时读共享变量允许一个线程获取写锁,也就是说不是所有写操作都会被阻塞。
需要注意,StampedLock
提供的是“乐观读”而不是“乐观读锁”,这表示乐观读是无锁的,这也是其比ReadWriteLock
读锁性能好的原因。
乐观读的使用示例
1 |
|
我们将共享变量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语句类似如下:
1 |
|
用户在生产订单UI执行保存操作的时候,后台利用下面的SQL语句更新生产订单,此处我们假设该条生产订单的version=4:
1 |
|
如果这条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() |
如果持有写锁,则释放该锁,而不需要戳值。这种方法可能对错误后的恢复很有用 |
语法
1 |
|
4.2 读模式
悲观的方式后去非独占读锁。
方法 | 说明 |
---|---|
readLock() |
阻塞等待获取非独占的读锁,返回一个戳, 如果是0表示获取失败 |
tryReadLock() |
尝试获取一个读锁,返回一个戳, 如果是0表示获取失败 |
long tryReadLock(long time, TimeUnit unit) |
尝试获取一个读锁,可以等待一段事件,返回一个戳, 如果是0表示获取失败 |
long readLockInterruptibly() |
阻塞等待获取非独占的读锁,可以被中断,返回一个戳, 如果是0表示获取失败 |
unlockRead(long stamp) |
释放非独占的读锁,传入之前获取的戳 |
tryUnlockRead() |
如果读锁被持有,则释放一次持有,而不需要戳值。这种方法可能对错误后的恢复很有用 |
语法
1 |
|
4.3 乐观读模式
StampedLock
支持 tryOptimisticRead()
方法,读取完毕后做一次戳校验,如果校验通过,表示这期间没有其他线程的写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据一致性。
方法 | 说明 |
---|---|
tryOptimisticRead() |
返回稍后可以验证的戳记,如果独占锁定则返回零 |
boolean validate(long stamp) |
如果自给定戳记发行以来锁还没有被独占获取,则返回true |
语法
1 |
|
此外,StampedLock 提供了api实现下面3种方式进行转换:
long tryConvertToWriteLock(long stamp)
验证当前锁版本和锁持有状态和给定的邮戳是否匹配,如果不匹配、邮戳的锁状态有误或当前持有多个共享锁则返回 0。匹配时则分三种情况,当前未持有锁则获取独占锁,当前持有独占锁则不进行操作,当前仅持有一个共享锁则释放共享锁获取独占锁,最终返回独占锁的邮戳。
long tryConvertToReadLock(long stamp)
验证当前锁版本和锁持有状态和给定的邮戳是否匹配,如果不匹配或者邮戳的锁状态有误则返回 0。匹配时则分三种情况,当前未持有锁则获取共享锁,当前持有独占锁则释放独占锁获取共享锁,当前持有共享锁则不进行操作,最终返回共享锁的邮戳。
long tryConvertToOptimisticRead(long stamp)
验证当前锁版本和锁持有状态和给定的邮戳是否匹配,如果匹配则进行一次锁释放,如果不匹配或者邮戳的锁状态有误则返回 0。该方法的逻辑和
unlock
方法的逻辑相似,如果当前未持有锁就直接返回锁版本,如果持有锁则进行一次锁释放,再返回锁版本。
五、读写案示例
提供一个数据容器类内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法
1 |
|
测试 读-读
可以优化
1 |
|
运行结果
1 |
|
结果分析
从结果中可以看到两个线程同时获取读锁并执行读操作,没有先后的关系。
测试 读-写
时优化读补加读锁
1 |
|
运行结果
1 |
|
结果分析
一开始是读操作先睡眠一秒,在睡眠之前已经获取了戳了,在 t1 线程睡眠期间 t2 线程获取到了写锁,并将数据修改,而且戳也改成了384。
此时 t1 线程醒过来校验发现戳已经被修改了,所以这时候 t1 线程会等待 t2 线程释放写锁之后去获取读锁。完成从乐观读 -> 读锁
的升级。
注意
StampedLock
不支持条件变量StampedLock
不支持可重入