【Java并发工具类】StampedLock:比读写锁更快的锁
前言
ReadWriteLock
适用于读多写少的场景,允许多个线程同时读取共享变量。但在读多写少的场景中,还有更快的技术方案。在Java 1.8中, 提供了StampedLock
锁,它的性能就比读写锁还要好。下面我们介绍StampedLock的使用方法、内部工作原理以及在使用过程中需要注意的事项。
StampedLock支持的三种锁模式
ReadWriteLock
支持两种访问模式:读锁和写锁,而StampedLock
支持三种访问模式:写锁、悲观读锁和乐观读。
其中写锁和悲观读锁的语义与ReadWriteLock中的写锁和读锁语义类似,允许多个线程同时获取悲观读锁,只允许一个线程获取写锁。与ReadWriteLock不同的是,StampedLock中的写锁和悲观读锁加锁成功之后,都会返回一个stamp标记,然后解锁的时候需要传入这个stamp。
相关示例代码如下(代码来自参考[1])
final StampedLock sl = new StampedLock();
// 获取/释放悲观读锁示意代码
long stamp = sl.readLock();
try {
//省略业务相关代码
} finally {
sl.unlockRead(stamp);
}
// 获取/释放写锁示意代码
long stamp = sl.writeLock();
try {
//省略业务相关代码
} finally {
sl.unlockWrite(stamp);
}
StampedLock的性能之所以比ReadWriteLock好,其关键在于StampedLock支持乐观读。ReadWriteLock支持多个线程同时读,当多个线程同时读的时候,所有的写操作都会被阻塞。但是,StampedLock提供了乐观读,当有多个线程同时读共享变量允许一个线程获取写锁,也就是说不是所有写操作都会被阻塞。
需要注意,StampedLock提供的是“乐观读”而不是“乐观读锁”,这表示乐观读是无锁的,这也是其比ReadWriteLock读锁性能好的原因。
乐观读的使用示例(代码来自参考[1]):
class Point{
private int x, y;
final StampedLock sl = new StampedLock();
// 计算到原点的距离
double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); //乐观读
//读取全局变量存储到局部变量中 在读入的过程中,数据可能被修改
int curX = x;
int curY = y;
//判断进行读操作期间,是否存在写操作,如果存在,则sl.validate(stamp)返回false
if(!sl.validate(stamp)) {
stamp = sl.readLock(); //升级为悲观读锁 一切的写操作都会被阻塞
try {
curX = x;
curY = y;
}finally {
sl.unlockRead(stamp); //释放悲观读锁
}
}
return Math.sqrt(curX*curX + curY*curY);
}
}
我们将共享变量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语句类似如下:
select id, ..., version
from product_doc
where id=777
用户在生产订单UI执行保存操作的时候,后台利用下面的SQL语句更新生产订单,此处我们假设该条生产订单的version=4:
update product_doc
set version=version+1,...
where id=777 and version=4
如果这条SQL语句执行成功并且返回条数等于1,那么说明从生产订单UI执行查询操作到执行保存期间,没有其他人修改过这条数据。因为如果这期间有人修改过这条数据,那么版本号字段一定会大于4。
数据库中的乐观锁,查询的时候,需要把version字段查出来,更新的时候要利用version字段做验证。StampedLock里面的stamp就类似于这个version字段。
StampedLock使用注意事项
StampedLock的功能仅仅是ReadWriteLock的子集,所以在使用时,还是需要注意一些地方:
StampedLock在命名上没有增加
Reentrant
,所以,猜想StampedLock不支持重入。事实上,确实如此,StampedLock是不支持重入的。StampedLock的悲观读锁、写锁都不支持条件变量。
如果线程阻塞在 StampedLock 的
readLock()
或者writeLock()
上时,调用该阻塞线程的interrupt()
方法,会导致 CPU 飙升。(代码来自参考[1])final StampedLock lock = new StampedLock();
Thread T1 = new Thread(()->{
lock.writeLock(); // 获取写锁
LockSupport.park(); // 永远阻塞在此处,不释放写锁
});
T1.start();
Thread.sleep(100); // 保证T1获取写锁
Thread T2 = new Thread(()->lock.readLock() ); //阻塞在悲观读锁
T2.start();
Thread.sleep(100); // 保证T2阻塞在读锁
//中断线程T2 会导致线程T2所在CPU飙升
T2.interrupt();
T2.join();
线程 T1 获取写锁之后将自己阻塞,线程 T2 尝试获取悲观读锁,也会阻塞;如果此时调用线程 T2 的 interrupt() 方法来中断线程 T2 的话,会发现线程 T2 所在 CPU 会飙升到 100%。(看专栏时明白线程T2获取悲观读锁会被阻塞,但是直到现在也不明白为什么调用T2的interrupt()方法会导致CPU飙升,望路过的看官解答。)
替代方法便是使用悲观读锁readLockInterruptibly()
和写锁writeLockInterruptibly()
。
StampedLock官方示例使用读写锁模板
精简Java官方示例后,可形成如下模板(代码来自参考[1])
StampedLock读模板:
final StampedLock sl = new StampedLock();
long stamp = sl.tryOptimisticRead(); // 乐观读
// 读入方法局部变量
//......
// 校验stamp
if (!sl.validate(stamp)){
stamp = sl.readLock(); // 升级为悲观读锁
try {
// 读入方法局部变量
.....
} finally {
sl.unlockRead(stamp); //释放悲观读锁
}
}
//使用方法局部变量执行业务操作
//......
StampedLock写模板:
long stamp = sl.writeLock();
try {
// 写共享变量
......
} finally {
sl.unlockWrite(stamp);
}
小结
这篇博客是学习专栏时的笔记总结出来的结果,粗略地介绍了一下StampedLock
,欲知更详细的请参考[3],无意中发现的大神博客,推荐起(•̀ᴗ•́)و ̑̑
参考:
[1] 极客时间专栏王宝令《Java并发编程实战》
[2] whoshiyeguiren.数据库乐观锁和悲观锁的理解和实现(转载&总结).https://blog.csdn.net/woshiyeguiren/article/details/80277475
[3] Ressmix.Java多线程进阶(十一)—— J.U.C之locks框架:StampedLock.https://segmentfault.com/a/1190000015808032?utm_source=tag-newest
【Java并发工具类】StampedLock:比读写锁更快的锁的更多相关文章
- 基于AQS实现的Java并发工具类
本文主要介绍一下基于AQS实现的Java并发工具类的作用,然后简单谈一下该工具类的实现原理.其实都是AQS的相关知识,只不过在AQS上包装了一下而已.本文也是基于您在有AQS的相关知识基础上,进行讲解 ...
- Java并发工具类 - CountDownLatch
Java并发工具类 - CountDownLatch 1.简介 CountDownLatch是Java1.5之后引入的Java并发工具类,放在java.util.concurrent包下面 http: ...
- 25.大白话说java并发工具类-CountDownLatch,CyclicBarrier,Semaphore,Exchanger
1. 倒计时器CountDownLatch 在多线程协作完成业务功能时,有时候需要等待其他多个线程完成任务之后,主线程才能继续往下执行业务功能,在这种的业务场景下,通常可以使用Thread类的join ...
- Java并发工具类CountDownLatch源码中的例子
Java并发工具类CountDownLatch源码中的例子 实例一 原文描述 /** * <p><b>Sample usage:</b> Here is a pai ...
- Java并发(十):读写锁ReentrantReadWriteLock
先做总结: 1.为什么用读写锁 ReentrantReadWriteLock? 重入锁ReentrantLock是排他锁,在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服 ...
- java 并发工具类CountDownLatch & CyclicBarrier
一起在java1.5被引入的并发工具类还有CountDownLatch.CyclicBarrier.Semaphore.ConcurrentHashMap和BlockingQueue,它们都存在于ja ...
- JAVA并发工具类---------------(CountDownLatch和CyclicBarrier)
CountDownLatch是什么 CountDownLatch,英文翻译为倒计时锁存器,是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待. 闭锁可以延迟线程的进 ...
- 【Java并发工具类】Semaphore
前言 1965年,荷兰计算机科学家Dijkstra提出的信号量机制成为一种高效的进程同步机制.这之后的15年,信号量一直都是并发编程领域的终结者.1980年,管程被提出,成为继信号量之后的在并发编程领 ...
- 【Java并发工具类】Java并发容器
前言 Java并发包有很大一部分都是关于并发容器的.Java在5.0版本之前线程安全的容器称之为同步容器.同步容器实现线程安全的方式:是将每个公有方法都使用synchronized修饰,保证每次只有一 ...
随机推荐
- 记录一下第一次用markdown写博客回滚过程
前面写博客,一直用的是博客园的TinyMCE编辑器, 今天不知道哪根筋搭牢了,想试试用Markdown写. 于是在“选项”里面把默认编辑器修改为“Markdown”,鉴于本人有一定的Markdown基 ...
- Nginx. 用http访问https跨域
用http 访问 https域名, 报跨越问题 解决方法: 在nginx相应服务的转发配置下添加: add_header 'Access-Control-Allow-Origin' 'http://i ...
- Numpy科学计算从放弃到入门
目录 一.什么是Numpy ndarray对象 相关文档 二.如何创建数组 三.如何访问数组 下标索引 切片索引 布尔型索引 整数索引 方形索引 四.如何做算数运算 五.如何使用统计函数 六.数组转置 ...
- Qt Installer Framework翻译(3-4)
更新组件 下图说明了用于更新已安装组件的默认工作流程: 本节使用在macOS上运行的Qt 5维护工具为例,来演示用户如何更新已安装组件. 启动更新程序 用户启动维护工具时,将打开"简介&qu ...
- Tesseract-OCR-v5.0中文识别,训练自定义字库,提高图片的识别效果
1,下载安装Tesseract-OCR 安装,链接地址https://digi.bib.uni-mannheim.de/tesseract/ 2,安装成功 tesseract -v 注意:安装后, ...
- 【Four-Week-Task】四周学习CTF之第一周【寒假更新】
写在最前:为了更好地系统学习CTF(楞头冲很惨 别问我怎么知道的 除非你是天才),决定先看再学,先正向再逆向. /* 出版排版规范中,标题序号等级为:第一级,一.二.三.(用顿号):第二级,(一).( ...
- 6、python基本数据类型之序列类型
前言:python的基本数据类型可以分为三类:数值类型.序列类型.散列类型,本文主要介绍序列类型及其通用操作. 一.序列类型 1)字符串(str):用单引号('),双引号("),三引号(三单 ...
- if continue的用法(跳过本次循环,执行下一个循环)
Python continue 语句跳出本次循环 当需要跳过本次循环的时候,使用continue能跳过本次循环,直接下一个循环 如下脚本: for url in alllink: if url == ...
- 看看AQS阻塞队列和条件队列
上一篇简单介绍了AQS,我们大概知道AQS就是一个框架,把很多功能都给实现了(比如入队规则,唤醒节点中的线程等),我们如果要使用的话只需要实现其中的一些方法(比如tryAcquire等)就行了!这次主 ...
- 画布 canvas 的相关内容
1.什么是canvas canvas也被叫做画布,是在JavaScript中完成网页图像制作的一个重要的途径,画布是一个矩形区域,在这个矩形区域中你可以利用好这里的每一个像素.同样在canvas中也有 ...