摘要:在读多写少的环境中,有没有一种比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:高并发场景下一种比读写锁更快的锁的更多相关文章

  1. 【Java并发工具类】StampedLock:比读写锁更快的锁

    前言 ReadWriteLock适用于读多写少的场景,允许多个线程同时读取共享变量.但在读多写少的场景中,还有更快的技术方案.在Java 1.8中, 提供了StampedLock锁,它的性能就比读写锁 ...

  2. HttpClient在高并发场景下的优化实战

    在项目中使用HttpClient可能是很普遍,尤其在当下微服务大火形势下,如果服务之间是http调用就少不了跟http客户端找交道.由于项目用户规模不同以及应用场景不同,很多时候可能不需要特别处理也. ...

  3. C++高并发场景下读多写少的解决方案

    C++高并发场景下读多写少的解决方案 概述 一谈到高并发的解决方案,往往能想到模块水平拆分.数据库读写分离.分库分表,加缓存.加mq等,这些都是从系统架构上解决.单模块作为系统的组成单元,其性能好坏也 ...

  4. C++高并发场景下读多写少的优化方案

    概述 一谈到高并发的优化方案,往往能想到模块水平拆分.数据库读写分离.分库分表,加缓存.加mq等,这些都是从系统架构上解决.单模块作为系统的组成单元,其性能好坏也能很大的影响整体性能,本文从单模块下读 ...

  5. Qunar机票技术部就有一个全年很关键的一个指标:搜索缓存命中率,当时已经做到了>99.7%。再往后,每提高0.1%,优化难度成指数级增长了。哪怕是千分之一,也直接影响用户体验,影响每天上万张机票的销售额。 在高并发场景下,提供了保证线程安全的对象、方法。比如经典的ConcurrentHashMap,它比起HashMap,有更小粒度的锁,并发读写性能更好。线程安全的StringBuilder取代S

    Qunar机票技术部就有一个全年很关键的一个指标:搜索缓存命中率,当时已经做到了>99.7%.再往后,每提高0.1%,优化难度成指数级增长了.哪怕是千分之一,也直接影响用户体验,影响每天上万张机 ...

  6. 【转】记录PHP、MySQL在高并发场景下产生的一次事故

    看了一篇网友日志,感觉工作中值得借鉴,原文如下: 事故描述 在一次项目中,上线了一新功能之后,陆陆续续的有客服向我们反应,有用户的个别道具数量高达42亿,但是当时一直没有到证据表示这是,确实存在,并且 ...

  7. 高并发场景下System.currentTimeMillis()的性能问题的优化 以及SnowFlakeIdWorker高性能ID生成器

    package xxx; import java.sql.Timestamp; import java.util.concurrent.*; import java.util.concurrent.a ...

  8. 高并发场景下System.currentTimeMillis()的性能问题的优化

    高并发场景下System.currentTimeMillis()的性能问题的优化 package cn.ucaner.alpaca.common.util.key; import java.sql.T ...

  9. 高并发场景下JVM调优实践之路

    一.背景 2021年2月,收到反馈,视频APP某核心接口高峰期响应慢,影响用户体验. 通过监控发现,接口响应慢主要是P99耗时高引起的,怀疑与该服务的GC有关,该服务典型的一个实例GC表现如下图: 可 ...

  10. MySQL在大数据、高并发场景下的SQL语句优化和"最佳实践"

    本文主要针对中小型应用或网站,重点探讨日常程序开发中SQL语句的优化问题,所谓“大数据”.“高并发”仅针对中小型应用而言,专业的数据库运维大神请无视.以下实践为个人在实际开发工作中,针对相对“大数据” ...

随机推荐

  1. RunnerGo可视化场景管理,还原真实场景

    在进行性能测试时,测试场景的正确配置非常关键.首先,需要根据业务场景和需求,设计出合理的测试场景,再利用相应的工具进行配置,实现自动化的性能测试. 在JMeter中,用户需要自己组织测试场景,或是在同 ...

  2. Linux报错:audit: backlog limit exceeded(审计:超出积压限制)

    Linux报错:audit: backlog limit exceeded(审计:超出积压限制) 系统版本:CentOS Linux release 7.6.1810 (Core) 问题现象:一次巡检 ...

  3. Synchronized和Volatile的对比

    Synchronized和Volatile是并发中的两大关键字,有相似性和不同点. Synchronized更详细介绍参考https://www.cnblogs.com/spark-cc/p/1706 ...

  4. WPF随笔收录-解析DICOM文件

    一.前言 在最近的项目开发中,涉及到了解析DICOM文件.根据百度百科可知,DICOM(Digital Imaging and Communications in Medicine)即医学数字成像和通 ...

  5. Ubuntu桌面顶端没有网络图标

    1 问题描述 刚装完新的linux系统后发现桌面顶部没有出现网络连接图标,网络无法进行连接配置. 2 解决方案 打开终端执行命令 nmcli network on或者sudo nmcli networ ...

  6. Win10系统总是锁屏关闭屏幕该怎么办?

    Win10系统总是锁屏关闭屏幕该怎么办? 转目前发现最早之帖,自脚本之家:https://www.jb51.net/os/win10/398451.html   使用Win10系统很长时间了,最近发现 ...

  7. NLP 开源形近字算法之相似字列表(番外篇)

    创作目的 国内对于文本的相似度计算,开源的工具是比较丰富的. 但是对于两个汉字之间的相似度计算,国内基本一片空白.国内的参考的资料少的可怜,国外相关文档也是如此. 本项目旨在抛砖引玉,实现一个基本的相 ...

  8. webrtc QOS笔记二 音频buffer数据不足生成很多gap的问题

    webrtc QOS笔记二 音频buffer数据不足生成很多gap的问题 目录 webrtc QOS笔记二 音频buffer数据不足生成很多gap的问题 记录个iusse. 插入音频数据后,GetAu ...

  9. AI工具导航

    .xe-comment-entry img { float: left; display: block; background: rgba(136, 136, 136, 0.15); margin-r ...

  10. Nordic芯片烧录指南

    本文讲介绍Nordic系列芯片的烧录方式 一.准备工作 1.硬件 首先需要准备一块Nordic的DK或者Jlink,但是需要注意的是x宝购买的盗版Jlink因为没有license,用一段时间可能会被锁 ...