比读写锁(ReadWriteLock)更快的锁(StampedLock)

﹏ヽ暗。殇╰゛Y 2022-08-30 11:20 261阅读 0赞

区别

ReadWriteLock 支持两种模式:

  • 读锁
  • 写锁。

而 StampedLock 支持三种模式:

  • 写锁
  • 悲观读锁(等价于ReadWriteLock 读锁)
  • 乐观读

这里可以看到StampedLock在命名上没有Reentrant前缀,所以StampedLock是不可重入锁

可以看到 StampedLock 也是和ReadWriteLock类似有读锁和写锁,也是运行多个线程同时获取悲观读锁,但是只运行一个线程获取写锁,写锁和悲观读锁是互斥的。不过有一点不同的是StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp

我们来看看使用案例

  1. // 获取读锁
  2. long stamp = lock.readLock();
  3. try {
  4. // 业务代码
  5. } finally {
  6. lock.unlock(stamp);
  7. }
  8. // 获取写锁
  9. long stamp1 = lock.writeLock();
  10. try {
  11. // 业务代码
  12. } finally {
  13. lock.unlockWrite(stamp1);
  14. }

可以看到使用悲观读锁和写锁本质上和ReadWriteLock没什么区别,那么StampedLock 的性能为什么会比 ReadWriteLock好呢

细心的读者会发现我们一直没有介绍上面的乐观读

StampedLock的乐观读和普通的读锁的区别是:普通的读锁在多个线程同时读的时候,所有的写操作会被阻塞,而 StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞,也就是说乐观读其实是无锁的。那具体要如何使用呢?我们来看一个简单的例子

  1. private double x, y;
  2. private final StampedLock sl = new StampedLock();
  3. //下面看看乐观读锁案例
  4. double distanceFromOrigin() {
  5. //获得一个乐观读锁
  6. long stamp = sl.tryOptimisticRead();
  7. //将两个字段读入本地局部变量,期间可能被其他线程修改值
  8. double currentX = x, currentY = y;
  9. //判断执行读操作期间,是否存在写操作,如果存在,则 sl.validate 返回 false
  10. if (!sl.validate(stamp)) {
  11. // 升级为悲观读锁
  12. stamp = sl.readLock();
  13. try {
  14. // 重新读取数据
  15. currentX = x;
  16. currentY = y;
  17. } finally {
  18. // 释放悲观读锁
  19. sl.unlockRead(stamp);
  20. }
  21. }
  22. // 注意这里还是会有并发冲突问题,但是使用validate能解决的问题是保证 在读取x, y 的时候值是正确的
  23. // 所以是否使用 StampedLock 还是要看实际业务场景 如果是强一致性还是要加读写锁
  24. return Math.sqrt(currentX * currentX + currentY * currentY);
  25. }

这里可以看到如果存在写操作,就会把乐观读升级为悲观读锁,然后重新读取数据,防止并发问题。看到这里是不是感觉有点鸡肋,不过还是要看有没有业务场景。

可以看到这里使用乐观读锁还是挺麻烦的,这里给出一套模板

  1. final StampedLock sl = new StampedLock();
  2. // 乐观读
  3. long stamp = sl.tryOptimisticRead();
  4. // 读入方法局部变量
  5. ......
  6. // 校验stamp
  7. if (!sl.validate(stamp)){
  8. // 升级为悲观读锁
  9. stamp = sl.readLock();
  10. try {
  11. // 读入方法局部变量
  12. .....
  13. } finally {
  14. //释放悲观读锁
  15. sl.unlockRead(stamp);
  16. }
  17. }
  18. //使用方法局部变量执行业务操作
  19. ......

不过有一点要注意的是如果线程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 方法,会导致 CPU 飙升。可以看下这个例子

  1. final StampedLock lock = new StampedLock();
  2. Thread t1 = new Thread(() -> {
  3. // 获取写锁
  4. lock.writeLock();
  5. // 永远阻塞在此处,不释放写锁
  6. LockSupport.park();
  7. });
  8. t1.start();
  9. // 保证T1获取写锁
  10. TimeUnit.SECONDS.sleep(100);
  11. //阻塞在悲观读锁
  12. Thread t2 = new Thread(lock::readLock);
  13. t2.start();
  14. // 保证T2阻塞在读锁
  15. TimeUnit.SECONDS.sleep(100);
  16. //中断线程T2
  17. //会导致线程T2所在CPU飙升
  18. t2.interrupt();
  19. t2.join();

不过一般我们都不会这么用,在使用的时候很少有需要响应中断的,如果非要使用。我们查看StampedLock的api会发现他有这么两个方法:悲观读锁 readLockInterruptibly()和写锁 writeLockInterruptibly()。所以如果要使用interrupt()方法就将相应的读写锁换成这两个即可。

发表评论

表情:
评论列表 (有 0 条评论,261人围观)

还没有评论,来说两句吧...

相关阅读

    相关 ReadWriteLock

    现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如

    相关 ReadWriteLock

           隐式锁Synchronized、重入锁ReetrantLock都是互斥锁、独占锁,即同一个锁只能每时每刻至多由一个线程来获持有。互斥,是一种保守策略,虽然避免了“

    相关 ReadWriteLock

    首先对于一个技术,存在就是为了解决某些技术难点。 为什么已经有ReentLock锁,却还要引入读写锁呢? 答案就是为了解决在 读多写少的场景下的性能问题,运用读写锁,能提高