高性能解决线程饥饿的利器 StampedLock

概览在 JDK 1.8 引入 StampedLock , 可以理解为对 ReentrantReadWriteLock 在某些方面的增强 , 在原先读写锁的基础上新增了一种叫乐观读(Optimistic Reading)的模式 。 该模式并不会加锁 , 所以不会阻塞线程 , 会有更高的吞吐量和更高的性能 。
跟着“码哥字节”带着问题一起来看StampedLock给我们带来了什么…

  • 有了ReentrantReadWriteLock , 为何还要引入StampedLock?
  • 什么是乐观读?
  • 在读多写少的并发场景下 , StampedLock如何解决写线程难以获取锁的线程“饥饿”问题?
  • 什么样的场景使用?
  • 实现原理分析 , 是通过 AQS 实现还是其他的?
特性它的设计初衷是作为一个内部工具类 , 用于开发其他线程安全的组件 , 提升系统性能 , 并且编程模型也比ReentrantReadWriteLock 复杂 , 所以用不好就很容易出现死锁或者线程安全等莫名其妙的问题 。
三种访问数据模式:
  • Writing(独占写锁):writeLock 方法会使线程阻塞等待独占访问 , 可类比ReentrantReadWriteLock 的写锁模式 , 同一时刻有且只有一个写线程获取锁资源;
  • Reading(悲观读锁):readLock方法 , 允许多个线程同时获取悲观读锁 , 悲观读锁与独占写锁互斥 , 与乐观读共享 。
  • Optimistic Reading(乐观读):这里需要注意了 , 是乐观读 , 并没有加锁 。 也就是不会有 CAS 机制并且没有阻塞线程 。 仅当当前未处于 Writing 模式 tryOptimisticRead才会返回非 0 的邮戳(Stamp) , 如果在获取乐观读之后没有出现写模式线程获取锁 , 则在方法validate返回 true, 允许多个线程获取乐观读以及读锁 。 同时允许一个写线程获取写锁 。
支持读写锁相互转换
ReentrantReadWriteLock 当线程获取写锁后可以降级成读锁 , 但是反过来则不行 。
StampedLock提供了读锁和写锁相互转换的功能 , 使得该类支持更多的应用场景 。
注意事项
  1. StampedLock是不可重入锁 , 如果当前线程已经获取了写锁 , 再次重复获取的话就会死锁;
  2. 都不支持 Conditon 条件将线程等待;
  3. StampedLock 的写锁和悲观读锁加锁成功之后 , 都会返回一个 stamp;然后解锁的时候 , 需要传入这个 stamp 。
详解乐观读带来的性能提升那为何 StampedLock 性能比 ReentrantReadWriteLock 好?
关键在于StampedLock 提供的乐观读 , 我们知道ReentrantReadWriteLock 支持多个线程同时获取读锁 , 但是当多个线程同时读的时候 , 所有的写线程都是阻塞的 。
StampedLock 的乐观读允许一个写线程获取写锁 , 所以不会导致所有写线程阻塞 , 也就是当读多写少的时候 , 写线程有机会获取写锁 , 减少了线程饥饿的问题 , 吞吐量大大提高 。
这里可能你就会有疑问 , 竟然同时允许多个乐观读和一个先线程同时进入临界资源操作 , 那读取的数据可能是错的怎么办?
是的 , 乐观读不能保证读取到的数据是最新的 , 所以将数据读取到局部变量的时候需要通过 lock.validate(stamp) 校验是否被写线程修改过 , 若是修改过则需要上悲观读锁 , 再重新读取数据到局部变量 。
同时由于乐观读并不是锁 , 所以没有线程唤醒与阻塞导致的上下文切换 , 性能更好 。
其实跟数据库的“乐观锁”有异曲同工之妙 , 它的实现思想很简单 。 我们举个数据库的例子 。
在生产订单的表 product_doc 里增加了一个数值型版本号字段 version , 每次更新 product_doc 这个表的时候 , 都将 version 字段加 1 。
select id , ..., versionfrom product_docwhere id = 123在更新的时候匹配 version 才执行更新 。
update product_docset version = version + 1,...where id = 123 and version = 5数据库的乐观锁就是查询的时候将 version 查出来 , 更新的时候利用 version 字段验证 , 若是相等说明数据没有被修改 , 读取的数据是安全的 。
这里的 version 就类似于 StampedLock 的 Stamp 。
使用示例模仿写一个将用户 id 与用户名数据保存在 共享变量 idMap 中 , 并且提供 put 方法添加数据、get 方法获取数据、以及 putIfNotExist 先从 map 中获取数据 , 若没有则模拟从数据库查询数据并放到 map 中 。
public class CacheStampedLock {/*** 共享变量数据*/private final Map idMap = new HashMap<>();private final StampedLock lock = new StampedLock();/*** 添加数据 , 独占模式*/public void put(Integer key, String value) {long stamp = lock.writeLock();try {idMap.put(key, value);} finally {lock.unlockWrite(stamp);}}/*** 读取数据 , 只读方法*/public String get(Integer key) {// 1. 尝试通过乐观读模式读取数据 , 非阻塞long stamp = lock.tryOptimisticRead();// 2. 读取数据到当前线程栈String currentValue = http://kandian.youth.cn/index/idMap.get(key);// 3. 校验是否被其他线程修改过,true 表示未修改 , 否则需要加悲观读锁if (!lock.validate(stamp)) {// 4. 上悲观读锁 , 并重新读取数据到当前线程局部变量stamp = lock.readLock();try {currentValue = idMap.get(key);} finally {lock.unlockRead(stamp);}}// 5. 若校验通过 , 则直接返回数据return currentValue;}/*** 如果数据不存在则从数据库读取添加到 map 中,锁升级运用* @param key* @param value 可以理解成从数据库读取的数据 , 假设不会为 null* @return*/public String putIfNotExist(Integer key, String value) {// 获取读锁 , 也可以直接调用 get 方法使用乐观读long stamp = lock.readLock();String currentValue = idMap.get(key);// 缓存为空则尝试上写锁从数据库读取数据并写入缓存try {while (Objects.isNull(currentValue)) {// 尝试升级写锁long wl = lock.tryConvertToWriteLock(stamp);// 不为 0 升级写锁成功if (wl != 0L) {// 模拟从数据库读取数据, 写入缓存中stamp = wl;currentValue = value;idMap.put(key, currentValue);break;} else {// 升级失败 , 释放之前加的读锁并上写锁 , 通过循环再试lock.unlockRead(stamp);stamp = lock.writeLock();}}} finally {// 释放最后加的锁lock.unlock(stamp);}return currentValue;}}