StampedLock:高并发场景下一种比读写锁更快的锁
摘要:在读多写少的环境中,有没有一种比ReadWriteLock更快的锁呢?有,那就是JDK1.8中新增的StampedLock!
本文分享自华为云社区《【高并发】高并发场景下一种比读写锁更快的锁》,作者: 冰 河。
什么是StampedLock?
ReadWriteLock锁允许多个线程同时读取共享变量,但是在读取共享变量的时候,不允许另外的线程多共享变量进行写操作,更多的适合于读多写少的环境中。那么,在读多写少的环境中,有没有一种比ReadWriteLock更快的锁呢?
答案当然是有!那就是我们今天要介绍的主角——JDK1.8中新增的StampedLock!没错,就是它!
StampedLock与ReadWriteLock相比,在读的过程中也允许后面的一个线程获取写锁对共享变量进行写操作,为了避免读取的数据不一致,使用StampedLock读取共享变量时,需要对共享变量进行是否有写入的检验操作,并且这种读是一种乐观读。
总之,StampedLock是一种在读取共享变量的过程中,允许后面的一个线程获取写锁对共享变量进行写操作,使用乐观读避免数据不一致的问题,并且在读多写少的高并发环境下,比ReadWriteLock更快的一种锁。
StampedLock三种锁模式
这里,我们可以简单对比下StampedLock与ReadWriteLock,ReadWriteLock支持两种锁模式:一种是读锁,另一种是写锁,并且ReadWriteLock允许多个线程同时读共享变量,在读时,不允许写,在写时,不允许读,读和写是互斥的,所以,ReadWriteLock中的读锁,更多的是指悲观读锁。
StampedLock支持三种锁模式:写锁、读锁(这里的读锁指的是悲观读锁)和乐观读(很多资料和书籍写的是乐观读锁,这里我个人觉得更准确的是乐观读,为啥呢?我们继续往下看啊)。其中,写锁和读锁与ReadWriteLock中的语义类似,允许多个线程同时获取读锁,但是只允许一个线程获取写锁,写锁和读锁也是互斥的。
另一个与ReadWriteLock不同的地方在于:StampedLock在获取读锁或者写锁成功后,都会返回一个Long类型的变量,之后在释放锁时,需要传入这个Long类型的变量。例如,下面的伪代码所示的逻辑演示了StampedLock如何获取锁和释放锁。
public class StampedLockDemo{
//创建StampedLock锁对象
public StampedLock stampedLock = new StampedLock();
//获取、释放读锁
public void testGetAndReleaseReadLock(){
long stamp = stampedLock.readLock();
try{
//执行获取读锁后的业务逻辑
}finally{
//释放锁
stampedLock.unlockRead(stamp);
}
}
//获取、释放写锁
public void testGetAndReleaseWriteLock(){
long stamp = stampedLock.writeLock();
try{
//执行获取写锁后的业务逻辑。
}finally{
//释放锁
stampedLock.unlockWrite(stamp);
}
}
}
StampedLock支持乐观读,这是它比ReadWriteLock性能要好的关键所在。 ReadWriteLock在读取共享变量时,所有对共享变量的写操作都会被阻塞。而StampedLock提供的乐观读,在多个线程读取共享变量时,允许一个线程对共享变量进行写操作。
我们再来看一下JDK官方给出的StampedLock示例,如下所示。
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) { // an exclusively locked method
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
double distanceFromOrigin() { // A read-only method
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
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);
}
}
}
在上述代码中,如果在执行乐观读操作时,另外的线程对共享变量进行了写操作,则会把乐观读升级为悲观读锁,如下代码片段所示。
double distanceFromOrigin() { // A read-only method
//乐观读
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
//判断是否有线程对变量进行了写操作
//如果有线程对共享变量进行了写操作
//则sl.validate(stamp)会返回false
if (!sl.validate(stamp)) {
//将乐观读升级为悲观读锁
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
//释放悲观锁
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
这种将乐观读升级为悲观读锁的方式相比一直使用乐观读的方式更加合理,如果不升级为悲观读锁,则程序会在一个循环中反复执行乐观读操作,直到乐观读操作期间没有线程执行写操作,而在循环中不断的执行乐观读会消耗大量的CPU资源,升级为悲观读锁是更加合理的一种方式。
StampedLock实现思想
StampedLock内部是基于CLH锁实现的,CLH是一种自旋锁,能够保证没有“饥饿现象”的发生,并且能够保证FIFO(先进先出)的服务顺序。
在CLH中,锁维护一个等待线程队列,所有申请锁,但是没有成功的线程都会存入这个队列中,每一个节点代表一个线程,保存一个标记位(locked),用于判断当前线程是否已经释放锁,当locked标记位为true时, 表示获取到锁,当locked标记位为false时,表示成功释放了锁。
当一个线程试图获得锁时,取得等待队列的尾部节点作为其前序节点,并使用类似如下代码判断前序节点是否已经成功释放锁:
while (pred.locked) {
//省略操作
}
只要前序节点(pred)没有释放锁,则表示当前线程还不能继续执行,因此会自旋等待;反之,如果前序线程已经释放锁,则当前线程可以继续执行。
释放锁时,也遵循这个逻辑,线程会将自身节点的locked位置标记为false,后续等待的线程就能继续执行了,也就是已经释放了锁。
StampedLock的实现思想总体来说,还是比较简单的,这里就不展开讲了。
StampedLock的注意事项
在读多写少的高并发环境下,StampedLock的性能确实不错,但是它不能够完全取代ReadWriteLock。在使用的时候,也需要特别注意以下几个方面。
StampedLock不支持重入
没错,StampedLock是不支持重入的,也就是说,在使用StampedLock时,不能嵌套使用,这点在使用时要特别注意。
StampedLock不支持条件变量
第二个需要注意的是就是StampedLock不支持条件变量,无论是读锁还是写锁,都不支持条件变量。
StampedLock使用不当会导致CPU飙升
这点也是最重要的一点,在使用时需要特别注意:如果某个线程阻塞在StampedLock的readLock()或者writeLock()方法上时,此时调用阻塞线程的interrupt()方法中断线程,会导致CPU飙升到100%。例如,下面的代码所示。
public void testStampedLock() throws Exception{
final StampedLock lock = new StampedLock();
Thread thread01 = new Thread(()->{
// 获取写锁
lock.writeLock();
// 永远阻塞在此处,不释放写锁
LockSupport.park();
});
thread01.start();
// 保证thread01获取写锁
Thread.sleep(100);
Thread thread02 = new Thread(()->
//阻塞在悲观读锁
lock.readLock()
);
thread02.start();
// 保证T2阻塞在读锁
Thread.sleep(100);
//中断线程thread02
//会导致线程thread02所在CPU飙升
thread02.interrupt();
thread02.join();
}
运行上面的程序,会导致thread02线程所在的CPU飙升到100%。
这里,有很多小伙伴不太明白为啥LockSupport.park();会导致thread01会永远阻塞。这里,冰河为你画了一张线程的生命周期图,如下所示。
这下明白了吧?在线程的生命周期中,有几个重要的状态需要说明一下。
- NEW:初始状态,线程被构建,但是还没有调用start()方法。
- RUNNABLE:可运行状态,可运行状态可以包括:运行中状态和就绪状态。
- BLOCKED:阻塞状态,处于这个状态的线程需要等待其他线程释放锁或者等待进入synchronized。
- WAITING:表示等待状态,处于该状态的线程需要等待其他线程对其进行通知或中断等操作,进而进入下一个状态。
- TIME_WAITING:超时等待状态。可以在一定的时间自行返回。
- TERMINATED:终止状态,当前线程执行完毕。
看完这个线程的生命周期图,知道为啥调用LockSupport.park();会使thread02阻塞了吧?
所以,在使用StampedLock时,一定要注意避免线程所在的CPU飙升的问题。那如何避免呢?
那就是使用StampedLock的readLock()方法或者读锁和使用writeLock()方法获取写锁时,一定不要调用线程的中断方法来中断线程,如果不可避免的要中断线程的话,一定要用StampedLock的readLockInterruptibly()方法获取可中断的读锁和使用StampedLock的writeLockInterruptibly()方法获取可中断的悲观写锁。
最后,对于StampedLock的使用,JDK官方给出的StampedLock示例本身就是一个最佳实践了,小伙伴们可以多看看JDK官方给出的StampedLock示例,多多体会下StampedLock的使用方式和背后原理与核心思想。
StampedLock:高并发场景下一种比读写锁更快的锁的更多相关文章
- 【Java并发工具类】StampedLock:比读写锁更快的锁
前言 ReadWriteLock适用于读多写少的场景,允许多个线程同时读取共享变量.但在读多写少的场景中,还有更快的技术方案.在Java 1.8中, 提供了StampedLock锁,它的性能就比读写锁 ...
- HttpClient在高并发场景下的优化实战
在项目中使用HttpClient可能是很普遍,尤其在当下微服务大火形势下,如果服务之间是http调用就少不了跟http客户端找交道.由于项目用户规模不同以及应用场景不同,很多时候可能不需要特别处理也. ...
- C++高并发场景下读多写少的解决方案
C++高并发场景下读多写少的解决方案 概述 一谈到高并发的解决方案,往往能想到模块水平拆分.数据库读写分离.分库分表,加缓存.加mq等,这些都是从系统架构上解决.单模块作为系统的组成单元,其性能好坏也 ...
- C++高并发场景下读多写少的优化方案
概述 一谈到高并发的优化方案,往往能想到模块水平拆分.数据库读写分离.分库分表,加缓存.加mq等,这些都是从系统架构上解决.单模块作为系统的组成单元,其性能好坏也能很大的影响整体性能,本文从单模块下读 ...
- Qunar机票技术部就有一个全年很关键的一个指标:搜索缓存命中率,当时已经做到了>99.7%。再往后,每提高0.1%,优化难度成指数级增长了。哪怕是千分之一,也直接影响用户体验,影响每天上万张机票的销售额。 在高并发场景下,提供了保证线程安全的对象、方法。比如经典的ConcurrentHashMap,它比起HashMap,有更小粒度的锁,并发读写性能更好。线程安全的StringBuilder取代S
Qunar机票技术部就有一个全年很关键的一个指标:搜索缓存命中率,当时已经做到了>99.7%.再往后,每提高0.1%,优化难度成指数级增长了.哪怕是千分之一,也直接影响用户体验,影响每天上万张机 ...
- 【转】记录PHP、MySQL在高并发场景下产生的一次事故
看了一篇网友日志,感觉工作中值得借鉴,原文如下: 事故描述 在一次项目中,上线了一新功能之后,陆陆续续的有客服向我们反应,有用户的个别道具数量高达42亿,但是当时一直没有到证据表示这是,确实存在,并且 ...
- 高并发场景下System.currentTimeMillis()的性能问题的优化 以及SnowFlakeIdWorker高性能ID生成器
package xxx; import java.sql.Timestamp; import java.util.concurrent.*; import java.util.concurrent.a ...
- 高并发场景下System.currentTimeMillis()的性能问题的优化
高并发场景下System.currentTimeMillis()的性能问题的优化 package cn.ucaner.alpaca.common.util.key; import java.sql.T ...
- 高并发场景下JVM调优实践之路
一.背景 2021年2月,收到反馈,视频APP某核心接口高峰期响应慢,影响用户体验. 通过监控发现,接口响应慢主要是P99耗时高引起的,怀疑与该服务的GC有关,该服务典型的一个实例GC表现如下图: 可 ...
- MySQL在大数据、高并发场景下的SQL语句优化和"最佳实践"
本文主要针对中小型应用或网站,重点探讨日常程序开发中SQL语句的优化问题,所谓“大数据”.“高并发”仅针对中小型应用而言,专业的数据库运维大神请无视.以下实践为个人在实际开发工作中,针对相对“大数据” ...
随机推荐
- 一站式微服务治理中台,Water v2.10.2 发布
Water(水孕育万物...) Water 为项目开发.服务治理,提供一站式解决方案(可以理解为微服务架构支持套件).基于 Solon 框架开发,并支持完整的 Solon Cloud 规范:已在生产环 ...
- 论文翻译:2023_THLNet: two-stage heterogeneous lightweight network for monaural speech enhancement
论文地址:THLNet: 用于单耳语音增强的两级异构轻量级网络 代码:https://github.com/dangf15/THLNet 引用格式:Dang F, Hu Q, Zhang P. THL ...
- Markdown 的常用语法
声明(叠甲):鄙人水平有限,本文章仅供参考. 且本文章是一个不全的分享,只有我目前用的比较多的语法,如果有什么错误,欢迎大家指正.具体可以查看该网站 写博客我使用的是 Markdown,为熟悉 Mar ...
- Java基础关于栈和堆的内存分配问题(转载)
AVA在程序运行时,在内存中划分5片空间进行数据的存储.分别是:1:寄存器.2:本地方法区.3:方法区.4:栈.5:堆. 基本,栈stack和堆heap这两个概念很重要,不了解清楚,后面就不用学了. ...
- 股票K线图概要——R语言
K线图是200多年前日本人发明的,后来传入了美国.K线图本来就是对当前数据的可视化显示而已,但是越来越多的人对K线图做出了痴迷的分析和解读,有众多追随者也根据K线图指导自己的具体行动,因此,K线图在实 ...
- Semantic Kernel 入门系列:🛸LLM降临的时代
不论你是否关心,不可否认,AGI的时代即将到来了. 在这个突如其来的时代中,OpenAI的ChatGPT无疑处于浪潮之巅.而在ChatGPT背后,我们不能忽视的是LLM(Large Language ...
- 欢迎使用园子的 vscode 插件
为了方便大家通过 vscode 编辑博文,我们做了一个小插件,插件名称是"博客园Cnblogs客户端",插件列表中搜索"博客园"或者 "cnblogs ...
- mysql数据库的登录脚本
######################## ku脚本: 可以使用以下ku脚本,它可以根据提供的参数登录到MySQL数据库: #!/bin/bash # Check for correct num ...
- 全网最详细中英文ChatGPT-GPT-4示例文档-智能聊天机器人从0到1快速入门——官网推荐的48种最佳应用场景(附python/node.js/curl命令源代码,小白也能学)
目录 Introduce 简介 setting 设置 Prompt 提示 Sample response 回复样本 API request 接口请求 python接口请求示例 node.js接口请求示 ...
- MySQL主从复制原理剖析与应用实践
vivo 互联网服务器团队- Shang Yongxing MySQL Replication(主从复制)是指数据变化可以从一个MySQL Server被复制到另一个或多个MySQL Server上, ...