读写锁还不会用 StampedLock 就 Out 了
概述
想到读写锁,大家第一时间想到的可能是ReentrantReadWriteLock
。实际上,在 jdk8 以后,java 提供了一个性能更优越的读写锁并发类StampedLock
,该类的设计初衷是作为一个内部工具类,用于辅助开发其它线程安全组件,用得好,该类可以提升系统性能,用不好,容易产生死锁和其它莫名其妙的问题。本文主要和大家一起学习下StampedLock
的功能和使用。
StampedLock 介绍
StampedLock
的状态由版本和模式组成。锁获取方法返回一个戳,该戳表示并控制对锁状态的访问。StampedLock
提供了 3 种模式控制访问锁:
写模式
获取写锁,它是独占的,当锁处于写模式时,无法获得读锁,所有乐观读验证都将失败。
writeLock(): 阻塞等待独占获取锁,返回一个戳, 如果是 0 表示获取失败。
tryWriteLock():尝试获取一个写锁,返回一个戳, 如果是 0 表示获取失败。
long tryWriteLock(long time, TimeUnit unit): 尝试获取一个独占写锁,可以等待一段事件,返回一个戳, 如果是 0 表示获取失败。
long writeLockInterruptibly(): 试获取一个独占写锁,可以被中断,返回一个戳, 如果是 0 表示获取失败。
unlockWrite(long stamp):释放独占写锁,传入之前获取的戳。
tryUnlockWrite():如果持有写锁,则释放该锁,而不需要戳值。这种方法可能对错误后的恢复很有用。
读模式
悲观的方式后去非独占读锁。
readLock(): 阻塞等待获取非独占的读锁,返回一个戳, 如果是 0 表示获取失败。
tryReadLock():尝试获取一个读锁,返回一个戳, 如果是 0 表示获取失败。
long tryReadLock(long time, TimeUnit unit): 尝试获取一个读锁,可以等待一段事件,返回一个戳, 如果是 0 表示获取失败。
long readLockInterruptibly(): 阻塞等待获取非独占的读锁,可以被中断,返回一个戳, 如果是 0 表示获取失败。
unlockRead(long stamp):释放非独占的读锁,传入之前获取的戳。
tryUnlockRead():如果读锁被持有,则释放一次持有,而不需要戳值。这种方法可能对错误后的恢复很有用。
乐观读模式
乐观读也就是若读的操作很多,写的操作很少的情况下,你可以乐观地认为,写入与读取同时发生几率很少,因此不悲观地使用完全的读取锁定,程序可以查看读取资料之后,是否遭到写入执行的变更,再采取后续的措施(重新读取变更信息,或者抛出异常) ,这一个小小改进,可大幅度提高程序的吞吐量。
StampedLock
支持 tryOptimisticRead()
方法,读取完毕后做一次戳校验,如果校验通过,表示这期间没有其他线程的写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据一致性。
tryOptimisticRead(): 返回稍后可以验证的戳记,如果独占锁定则返回零。
boolean validate(long stamp): 如果自给定戳记发行以来锁还没有被独占获取,则返回 true。
此外,StampedLock 提供了 api 实现上面 3 种方式进行转换:
long tryConvertToWriteLock(long stamp)
如果锁状态与给定的戳记匹配,则执行以下操作之一。如果戳记表示持有写锁,则返回它。或者,如果是读锁,如果写锁可用,则释放读锁并返回写戳记。或者,如果是乐观读,则仅在立即可用时返回写戳记。该方法在所有其他情况下返回零
long tryConvertToReadLock(long stamp)
如果锁状态与给定的戳记匹配,则执行以下操作之一。如果戳记表示持有写锁,则释放它并获得读锁。或者,如果是读锁,返回它。或者,如果是乐观读,则仅在立即可用时才获得读锁并返回读戳记。该方法在所有其他情况下返回零。
long tryConvertToOptimisticRead(long stamp)
如果锁状态与给定的戳记匹配,那么如果戳记表示持有锁,则释放它并返回一个观察戳记。或者,如果是乐观读,则在验证后返回它。该方法在所有其他情况下返回 0,因此作为“tryUnlock”的形式可能很有用。
演示例子
下面用一个例子演示下 StampedLock 的使用,例子来源 jdk 中的 javadoc。
测试用例:
结果:
性能对比
正是由于StampedLock
的乐观读模式,早就StampedLock
的高性能和高吞吐量,那么具体的性能提高有多少呢?
下图是和 ReadWritLock 相比,在一个线程情况下,读速度是其 4 倍左右,写是 1 倍。
下图是 16 个线程情况下,读性能是其几十倍,写性能也是近 10 倍左右:
下图是吞吐量提高:
那么这样是不是说StampedLock
可以全方位的替代ReentrantReadWriteLock
, 答案是否定的,StampedLock
相对于ReentrantReadWriteLock
有下面两个问题:
不支持条件变量 Condition
不支持可重入
所以最终选择StampedLock
还是ReentrantReadWriteLock
,还是要看具体的业务场景。
总结
本文主要讲解了StampedLock
的功能和使用,至于原理,StampedLock
虽然不像其它锁一样定义了内部类来实现 AQS 框架,但是 StampedLock 的基本实现思路还是利用 CLH 队列进行线程的管理,通过同步状态值来表示锁的状态和类型,具体的源码实现大家感兴趣的自己可以追踪看看。
参考
https://pdai.tech/md/java/java8/java8-stampedlock.htmlhttps://cloud.tencent.com/developer/article/1470988
https://www.liaoxuefeng.com/wiki/1252599548343744/1309138673991714
版权声明: 本文为 InfoQ 作者【JAVA旭阳】的原创文章。
原文链接:【http://xie.infoq.cn/article/6eb9067f1e9ea4610da05eba9】。文章转载请联系作者。
评论