首页 > 其他分享 >读写锁还不会用StampedLock就Out了

读写锁还不会用StampedLock就Out了

时间:2022-10-19 13:33:10浏览次数:54  
标签:返回 stamp 读写 long StampedLock 获取 读锁 Out

概述

想到读写锁,大家第一时间想到的可能是​​ReentrantReadWriteLock​​。实际上,在jdk8以后,java提供了一个性能更优越的读写锁并发类​​StampedLock​​,该类的设计初衷是作为一个内部工具类,用于辅助开发其它线程安全组件,用得好,该类可以提升系统性能,用不好,容易产生死锁和其它莫名其妙的问题。本文主要和大家一起学习下​​StampedLock​​的功能和使用。

StampedLock介绍

​StampedLock​​的状态由版本和模式组成。锁获取方法返回一个戳,该戳表示并控制对锁状态的访问。​​StampedLock​​提供了3种模式控制访问锁:

  1. 写模式

获取写锁,它是独占的,当锁处于写模式时,无法获得读锁,所有乐观读验证都将失败。

  • writeLock(): 阻塞等待独占获取锁,返回一个戳, 如果是0表示获取失败。
  • tryWriteLock():尝试获取一个写锁,返回一个戳, 如果是0表示获取失败。
  • long tryWriteLock(long time, TimeUnit unit): 尝试获取一个独占写锁,可以等待一段事件,返回一个戳, 如果是0表示获取失败。
  • long writeLockInterruptibly(): 试获取一个独占写锁,可以被中断,返回一个戳, 如果是0表示获取失败。
  • unlockWrite(long stamp):释放独占写锁,传入之前获取的戳。
  • tryUnlockWrite():如果持有写锁,则释放该锁,而不需要戳值。这种方法可能对错误后的恢复很有用。
long stamp = lock.writeLock();
try {
....
} finally {
lock.unlockWrite(stamp);
}
  1. 读模式

悲观的方式后去非独占读锁。

  • readLock(): 阻塞等待获取非独占的读锁,返回一个戳, 如果是0表示获取失败。
  • tryReadLock():尝试获取一个读锁,返回一个戳, 如果是0表示获取失败。
  • long tryReadLock(long time, TimeUnit unit): 尝试获取一个读锁,可以等待一段事件,返回一个戳, 如果是0表示获取失败。
  • long readLockInterruptibly(): 阻塞等待获取非独占的读锁,可以被中断,返回一个戳, 如果是0表示获取失败。
  • unlockRead(long stamp):释放非独占的读锁,传入之前获取的戳。
  • tryUnlockRead():如果读锁被持有,则释放一次持有,而不需要戳值。这种方法可能对错误后的恢复很有用。
long stamp = lock.readLock();
try {
....
} finally {
lock.unlockRead(stamp);
}
  1. 乐观读模式

乐观读也就是若读的操作很多,写的操作很少的情况下,你可以乐观地认为,写入与读取同时发生几率很少,因此不悲观地使用完全的读取锁定,程序可以查看读取资料之后,是否遭到写入执行的变更,再采取后续的措施(重新读取变更信息,或者抛出异常) ,这一个小小改进,可大幅度提高程序的吞吐量。

​StampedLock​​ 支持 ​​tryOptimisticRead()​​ 方法,读取完毕后做一次戳校验,如果校验通过,表示这期间没有其他线程的写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据一致性。

  • tryOptimisticRead(): 返回稍后可以验证的戳记,如果独占锁定则返回零。
  • boolean validate(long stamp): 如果自给定戳记发行以来锁还没有被独占获取,则返回true。
long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)){
// 锁升级
}

此外,StampedLock 提供了api实现上面3种方式进行转换:

  • long tryConvertToWriteLock(long stamp)

如果锁状态与给定的戳记匹配,则执行以下操作之一。如果戳记表示持有写锁,则返回它。或者,如果是读锁,如果写锁可用,则释放读锁并返回写戳记。或者,如果是乐观读,则仅在立即可用时返回写戳记。该方法在所有其他情况下返回零

  • long tryConvertToReadLock(long stamp)

如果锁状态与给定的戳记匹配,则执行以下操作之一。如果戳记表示持有写锁,则释放它并获得读锁。或者,如果是读锁,返回它。或者,如果是乐观读,则仅在立即可用时才获得读锁并返回读戳记。该方法在所有其他情况下返回零。

  • long tryConvertToOptimisticRead(long stamp)

如果锁状态与给定的戳记匹配,那么如果戳记表示持有锁,则释放它并返回一个观察戳记。或者,如果是乐观读,则在验证后返回它。该方法在所有其他情况下返回0,因此作为“tryUnlock”的形式可能很有用。

演示例子

下面用一个例子演示下StampedLock的使用,例子来源jdk中的javadoc。

@Slf4j
@Data
public class Point {
private double x, y;
private final StampedLock sl = new StampedLock();

void move(double deltaX, double deltaY) throws InterruptedException {
//涉及对共享资源的修改,使用写锁-独占操作
long stamp = sl.writeLock();
log.info("writeLock lock success");
Thread.sleep(500);
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
log.info("unlock write lock success");
}
}

/**
* 使用乐观读锁访问共享资源
* 注意:乐观读锁在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候可能其他写线程已经修改了数据,
* 而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。
*
* @return
*/
double distanceFromOrigin() throws InterruptedException {
long stamp = sl.tryOptimisticRead(); // 使用乐观读锁
log.info("tryOptimisticRead lock success");
// 睡一秒中
Thread.sleep(1000);
double currentX = x, currentY = y; // 拷贝共享资源到本地方法栈中
if (!sl.validate(stamp)) { // 如果有写锁被占用,可能造成数据不一致,所以要切换到普通读锁模式
log.info("validate stamp error");
stamp = sl.readLock();
log.info("readLock success");
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
log.info("unlock read success");
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}

void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) {
long ws = sl.tryConvertToWriteLock(stamp); //读锁转换为写锁
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
} else {
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}
}

测试用例:

@Test
public void testStamped() throws InterruptedException {
Point point = new Point();
point.setX(1);
point.setY(2);
// 线程0 执行了乐观读
Thread thread0 = new Thread(() -> {
try {
// 乐观读
point.distanceFromOrigin();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread-0");
thread0.start();

Thread.sleep(500);
// 线程1 执行写锁
Thread thread1 = new Thread(() -> {
// 乐观读
try {
point.move(3, 4);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread-1");
thread1.start();

thread0.join();
thread1.join();
}

结果:

读写锁还不会用StampedLock就Out了_读锁

性能对比

正是由于​​StampedLock​​的乐观读模式,早就​​StampedLock​​的高性能和高吞吐量,那么具体的性能提高有多少呢?

下图是和ReadWritLock相比,在一个线程情况下,读速度是其4倍左右,写是1倍。

读写锁还不会用StampedLock就Out了_数据_02

下图是16个线程情况下,读性能是其几十倍,写性能也是近10倍左右:

读写锁还不会用StampedLock就Out了_读写锁_03

下图是吞吐量提高:

读写锁还不会用StampedLock就Out了_数据_04

那么这样是不是说​​StampedLock​​可以全方位的替代​​ReentrantReadWriteLock​​, 答案是否定的,​​StampedLock​​相对于​​ReentrantReadWriteLock​​有下面两个问题:

  1. 不支持条件变量Condition
  2. 不支持可重入

所以最终选择​​StampedLock​​还是​​ReentrantReadWriteLock​​,还是要看具体的业务场景。

总结

本文主要讲解了​​StampedLock​​的功能和使用,至于原理,​​StampedLock​​虽然不像其它锁一样定义了内部类来实现AQS框架,但是StampedLock的基本实现思路还是利用CLH队列进行线程的管理,通过同步状态值来表示锁的状态和类型,具体的源码实现大家感兴趣的自己可以追踪看看。

标签:返回,stamp,读写,long,StampedLock,获取,读锁,Out
From: https://blog.51cto.com/u_15773567/5769683

相关文章

  • .netcore里使用StackExchange.Redis TimeOut 情况解决方法
    在用StackExchange.Redis这个组件时候,时不时会出现异常TimeOut解决方法如下,解决方法: 在Program的Main入口方法里添加一句话:200,200); 比如以下代码:publicclassProg......
  • react-router-dom(v6)快速上手
    本篇笔记跟随官方教程而写,把其中连续的步骤碎片化,方便以后忘记的时候直接调用,其中包含了对其他小问题的拓展。创建路由(quick-start)npminstallreact-router-dom在R......
  • Vue 插件:VueRouter
    VueRouter是一个Vue插件,用于实现SPA(singlepagewebapplication)应用。SPA(singlepagewebapplication)应用,即单页面应用。整个应用只有一个.html文件,通常命名为......
  • 三阶段 vue 路由 $route 和 $router 的区别
    1.这是vue-router提供给我们的实例实例的两个属性(api)2.$route是路由对象,一般是获取动态参数|querythis.$route.params.idthis./$route.title ......
  • cpp ofstream ifstreram binary 文档读写
    includeincludeincludeincludeinclude<sys/stat.h>includevoidtest_copy_binary(std::stringsrc,std::stringdst){std::ofstreamostrm(dst,std::ios......
  • 升级到React-Router-v6
    前言近期完成了公司新项目的开发,相关的技术栈都用到了最新版本,reactrouter也使用了v6的版本,所以借这个机会自己再梳理下reactrouterv5与v6的区别,以及v6一些新......
  • vue-router中query和params的区别
    一.query和params的知识点理解/data/:id这个路由匹配/data/1,/data/2这里的id叫params/data?id=1/data?id=2这里的id叫queryparams方法传参时,要在路由后面加......
  • React的useLayoutEffect和useEffect执行时机有什么不同
    我们先看下React官方文档对这两个hook的介绍,建立个整体认识useEffect(create,deps):该Hook接收一个包含命令式、且可能有副作用代码的函数。在函数组件主体内(这......
  • Airtest之用装饰器重试解决Poco RpcTimeoutError
    上期回顾:Airtest自动化多设备测试框架DreamMultiDevices以下基于python3.8;airtestIDE1.2.14;airtest1.2.6;pocoui1.0.87运行Poco脚本的时候,有没有遇到过RpcTimeout的报......
  • 解决node环境下SyntaxError: Cannot use import statement outside a module的问题
    问题今天刷leetcode的时候,发现有些函数老是重复书写,于是单独写了一个uitls.js模块,通过import的方式导入,没想到居然报错了。解决方案错误警告其实已经给出了解决方案,在pac......