深入理解Disruptor

news2025/1/16 16:02:40

Disruptor通过缓存行填充,利用CPU高速缓存,只是Disruptor“快”的一个因素,快的另一因素是“无锁”,尽可能发挥CPU本身的高速处理性能。

1 缓慢的锁

Disruptor作为一个高性能的生产者-消费者队列系统,核心就是通过RingBuffer实现一个无锁队列。

Jdk像LinkedBlockingQueue队列库,比Disruptor RingBuffer慢很多。

1.1 链表数据在内存布局对高速缓存不友好

RingBuffer使用数组:

1.2 锁依赖

生产者-消费者模式里,可能有多个消费者,也可能多个生产者。
多个生产者都要往队尾指针添加新任务,产生多线程竞争。于是,做这事时,生产者就要拿到对队尾的锁。同样多个消费者去消费队头时,也就产生竞争。同样消费者也要拿到锁。

那只有一个生产者或一个消费者,是不是就没锁的竞争问题?No!生产者-消费者模式下,消费者比生产者快。不然,队列会积压,任务越堆越多:

  • 越来越多的任务没能及时完成
  • 内存也放不下

虽然生产者-消费者模型下,都有队列作为缓冲区,但大部分情况下,这缓冲区空。即使只有一个生产者和一个消费者,这生产者指向的队尾和消费者指向的队头是同一节点。于是,这两个生产者和消费者之间一样产生锁竞争。

在LinkedBlocking Queue锁机制通过ReentrantLock实现。用Java在JVM上直接实现的加锁机制,由JVM进行裁决。这锁的争夺,会把没有拿到锁的线程挂起等待,也需经过一次上下文切换(Context Switch)。

这上下文切换要做的和异常和中断里的一样。上下文切换过程,要把当前执行线程的寄存器等信息,保存到线程栈。即已加载到高速缓存的指令或数据,又回到主内存,会进步拖慢性能。

Disruptor的Benchmark测试:把一个long类型counter,从0自增到5亿

  • 一种方式没任何锁
  • 另外一个方式每次自增时都取一个锁

分别207毫秒和9603毫秒,性能差近50倍。

import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


public class LockBenchmark {


    public static void runIncrement() {
        long counter = 0;
        long max = 500000000L;
        long start = System.currentTimeMillis();
        while (counter < max) {
            counter++;
        }
        long end = System.currentTimeMillis();
        System.out.println("Time spent is " + (end-start) + "ms without lock");
    }


    public static void runIncrementWithLock() {
        Lock lock = new ReentrantLock();
        long counter = 0;
        long max = 500000000L;
        long start = System.currentTimeMillis();
        while (counter < max) {
            if (lock.tryLock()){
                counter++;
                lock.unlock();
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("Time spent is " + (end-start) + "ms with lock");
    }


    public static void main(String[] args) {
        runIncrement();
        runIncrementWithLock();
    }
}

加锁和不加锁自增counter
Time spent is 207ms without lock
Time spent is 9603ms with lock
性能差出将近10倍

2 无锁的RingBuffer

加锁很慢,所以Disruptor“无锁”,即没有os层的锁。
Disruptor还利用CPU硬件支持的指令,CAS,Intel CPU对应指令 cmpxchg。

和直接在链表的头和尾加锁不同,RingBuffer创建一个Sequence对象,指向当前的RingBuffer的头和尾。这头和尾的标识不是通过指针实现,而是通过序号,类名叫Sequence。

RingBuffer中进行生产者和消费者之间的资源协调,是对比序号。
当Pro想往队列加新数据,它会把当前Pro的Sequence的序号,加上需要加入的新数据的数量,然后和实际的消费者所在的位置对比,看队列里是否有足够空间加入这些数据,而不会覆盖消费者还没处理完的数据。

Sequence就是通过CAS,即UNSAFE.compareAndSwapLong:

 public boolean compareAndSet(final long expectedValue, final long newValue)
	    {
	        return UNSAFE.compareAndSwapLong(this, VALUE_OFFSET, expectedValue, newValue);
	    }


public long addAndGet(final long increment)
    {
        long currentValue;
        long newValue;


        do
        {
            currentValue = get();
            newValue = currentValue + increment;
        }
        while (!compareAndSet(currentValue, newValue));


        return newValue;

Sequence源码中的addAndGet,若CAS失败,会不断忙等待重试。
CAS不是基础库函数,也不是os实现的一个系统调用,而是一个CPU硬件支持的机器指令。Intel CPU的cmpxchg指令,compxchg [ax] (隐式参数,EAX累加器), [bx] (源操作数地址), [cx] (目标操作数地址):

  • 第一个操作数不在指令里面出现,是一个隐式的操作数,也就是EAX累加寄存器里面的值
  • 第二个操作数就是源操作数,并且指令会对比这个操作数和上面的累加寄存器里面的值

若值相同,CPU会把ZF(条件码寄存器里零标志位的值)置1,再把第三个操作数(即目标操作数)设置到源操作数的地址。

不相等,就会把源操作数里的值,设置到累加器寄存器。

对应伪代码:

IF [ax]< == [bx] THEN [ZF] = 1, [bx] = [cx]
                 ELSE [ZF] = 0, [ax] = [bx] 

单指令是原子的,即CAS时,无需再加锁,直接调用。无锁,CPU就像在赛道上行驶,不会遇到需上下文切换红灯而停下来。虽会遇到CAS这样复杂机器指令,就好像赛道上会有U型弯,不过不用完全停等待,CPU运行起来仍快得多。

3 CAS到底多快

import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


public class LockBenchmark {


    public static void runIncrementAtomic()
    {
        AtomicLong counter = new AtomicLong(0);
        long max = 500000000L;
        long start = System.currentTimeMillis();
        while (counter.incrementAndGet() < max) {
        }
        long end = System.currentTimeMillis();
        System.out.println("Time spent is " + (end-start) + "ms with cas");
    }


    public static void main(String[] args) {
        runIncrementAtomic();
    }
}


Time spent is 3867ms with cas

incrementAndGet最终到CPU指令层面,就是CAS操作。它所花费时间,虽比没任何锁的操作慢一个数量级,但比使用ReentrantLock这样的操作系统锁的机制,还是减少一半时间。

4 总结

Java基础库里面的BlockingQueue,都要通过显示地加锁来保障生产者之间、消费者之间,乃至生产者和消费者之间,不会发生锁冲突的问题。

但加锁会大大拖慢性能。获取锁时,CPU没有执行计算相关指令,而要等待os或JVM进行锁竞争裁决。那些没有拿到锁而被挂起等待的线程,则需上下文切换。这上下文切换,会把挂起线程的寄存器里的数据放到线程的程序栈。即加载到高速缓存里面的数据也失效了,程序就变得更慢。

RingBuffer采用无锁方案,通过CAS进行序号自增和对比,使CPU无需获取os锁。而能继续顺序执行CPU指令。无上下文切换、os锁,程序就快。不过因为采用CAS忙等待(Busy-Wait),会使得CPU始终满负荷运转,消耗更多电,小缺点。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/162231.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

面向对象的好处

提到面向对象的好处&#xff0c;一些人脑中可能会冒出&#xff1a;封装继承多态封装 封装&#xff1a;通过类&#xff0c;为数据和方法&#xff0c;提供统一的上下文 但是&#xff0c;函数名&#xff0c;同样也可以提供上下文&#xff0c;并且可以通过一种叫柯里化的技巧&…

比特位计数[动态规划 || bitCount计数]

二进制计数前言一、二进制计数二、动态规划 & bitCount分治统计1、bitCount分治统计2、动态规划总结参考文献前言 二进制计数可以直接基于分治去快速统计&#xff0c;如果是连续数的二进制计数&#xff0c;可以利用前面已经计算出的状态进行递推求解&#xff0c;即动态规划…

Python NumPy 连接数组

前言NumPy&#xff08;Numerical Python的缩写&#xff09;是一个开源的Python科学计算库。使用NumPy&#xff0c;就可以很自然地使用数组和矩阵。NumPy包含很多实用的数学函数&#xff0c;涵盖线性代数运算、傅里叶变换和随机数生成等功能。本文主要介绍Python NumPy 连接数组…

使用Java为何总写出C风格的代码?

“你看你所有代码都是把字段取出来计算&#xff0c;然后&#xff0c;再塞回去。各种不同层面的业务计算混在一起&#xff0c;将来有一点调整&#xff0c;所有代码都得跟着变。” 在实际的开发过程中&#xff0c;有不少人都这么写代码的。Java写的代码应该有Java的风格&#xf…

Karl Guttag:Quest Pro透视效果差,并不适合商用

AR/VR光学专家Karl Guttag曾指出&#xff0c;基于VST透视的AR虽然绕开了光学透视AR的一些局限&#xff0c;但VST透视依然存在运动延迟、余光视觉透视效果、分辨率、IPD不匹配等多种问题。的确&#xff0c;VST透视AR的结构、原理比光学透视AR更简单&#xff0c;但它同样需要解决…

(二十)正则表达式

目录 前言: 1.概述: 2.正则表达式体验: 3.正则表达式字符 4.正则表达式在字符串方法中的使用 5.代码演示: 6.正则表达式支持爬取信息 7.代码演示: 前言: 正则表达式&#xff0c;又称规则表达式,&#xff08;Regular Expression&#xff0c;在代码中常简写为regex、regex…

SpringCloud-Netflix学习笔记05——Eureka模拟实现简单集群

前言 对于Eureka注册中心来说&#xff0c;如果只有一个注册中心的话&#xff0c;如果注册中心崩了&#xff0c;那么里面的服务全部用不了&#xff0c;系统就会崩溃。为了避免这个问题&#xff0c;我们可以搭建一个注册中心的集群&#xff0c;几个注册中心互相关联&#xff0c;如…

程序员别死背面试八股文了,这种面试题才是未来主流。。。

目录&#xff1a; 面试官为啥要出这样一个开放式问题生产消费模型及核心数据结构支撑TB级数据写入的分布式架构数据宕机场景下的高可用架构支持数据不丢失的ack机制最后的总结 1、面试官为啥要出这样一个开放式问题 这篇文章简单给大家来聊一个互联网大厂的Java面试题&#x…

【Git 从入门到精通】一文摸透Git中的分支操作

文章目录一、什么是分支&#xff1f;二、分支中的常用命令三、上手分支1.查看分支2.创建分支3.修改分支4.切换分支5.合并分支6.解决冲突四、分支操作原理分析一、什么是分支&#xff1f; 在版本控制过程中&#xff0c;同时推进多个任务&#xff0c;为每个任务&#xff0c;我们…

肠道核心菌——戴阿利斯特杆菌属 (Dialister)

谷禾健康 戴阿利斯特杆菌属 &#xff08;Dialister&#xff09; ✦ Dialister&#xff08;戴阿利斯特杆菌属&#xff09;是小的、厌氧或微需氧的革兰氏阴性球状或杆状菌&#xff0c;因次也被翻译成小杆菌属。 Dialister菌是人体肠道菌群中的一种常见菌种。该菌属物种被发现出现…

基于 Hutool 的抽奖实现与原理

前言 先大概描述下 hutool 工具是如何根据权重进行抽取&#xff0c;后面再结合源码进行讲解。 假设有如下奖品以及对应的权重&#xff1a; 奖品名称权重奖品数量谢谢参与0.76010积分0.4550IPhone 140.055Mac Book Air0.011 需要注意 谢谢参与 也算是一种奖品&#xff0c;因为…

SpringCloud-Netflix学习笔记04——Eureka注册中心搭建

前言 Eureka注册中心相当于Zookeeper注册中心&#xff0c;思想是类似的&#xff0c;只不过Zookeeper需要在本机上下载一个服务客户端&#xff0c;直接启动客户端即可&#xff0c;而Eureka注册中心需要我们自己动手搭建&#xff0c;不过也不难。 搭建步骤 1、新建一个Maven项目…

PySpark数据计算中常用的成员方法(算子)

目录 一.回顾 二.数据计算 map算子 演示 flatMap算子 演示 reduceByKey算子 演示 练习案例1 需求 解决步骤 完整代码 filter算子 演示 distinct算子 演示 sortBy算子 演示 练习案例2 解决步骤 完整代码 三.总结 一.回顾 1.RDD对象是什么?为什么要使用它? RDD对象称…

SegFormer学习笔记(1)安装

一、源码&#xff1a;https://github.com/sithu31296/semantic-segmentation我并没使用SegFormer的官方源码&#xff0c;那个mmcv特磨人了&#xff0c;各种奇葩配置错误。二、环境配置新建conda环境conda create -n segformer3715 python3.7.15 选用python3.7.15(纯粹的3.7.0版…

计算机原理四_内存管理

目录儿三、内存管理3.1 内存管理基础3.1.1存储器的多层结构3.1.2 进程运行基本原理进程的装入3.1.3 内存扩充3.1.4 内存的分配3.1.4.1连续分配3.1.4.2非连续分配3.1.4.2.1基本分页存储管理3.1.4.2.2基本分段存储管理3.1.4.2.3 段页式管理3.2 虚拟内存管理3.2.1 虚拟内存的概念3…

【BP靶场portswigger-客户端11】跨站点脚本XSS-10个实验(下)

前言&#xff1a; 介绍&#xff1a; 博主&#xff1a;网络安全领域狂热爱好者&#xff08;承诺在CSDN永久无偿分享文章&#xff09;。 殊荣&#xff1a;CSDN网络安全领域优质创作者&#xff0c;2022年双十一业务安全保卫战-某厂第一名&#xff0c;某厂特邀数字业务安全研究员&…

【Go基础】函数和面向接口编程

文章目录一、函数1. 函数的基本形式2. 递归函数3. 匿名函数4. 闭包5. 延迟调用defer6. 异常处理二、面向接口编程1. 接口的基本概念2. 接口的使用3. 接口的赋值4. 接口嵌入5. 空接口6. 类型断言7. 面向接口编程一、函数 1. 函数的基本形式 // 函数定义&#xff1a;a,b是形参 …

【测试】自动化测试

努力经营当下&#xff0c;直至未来明朗&#xff01; 文章目录一、自动化概述二、自动化测试的分类三、自动化测试工具&#xff1a;selenium四、一个简单的自动化用例五、Selenium常用方法1. 查找页面元素&#xff1a;2.元素的定位&#xff08;By类&#xff09;小结普通小孩也要…

Java中this的用法

一、this关键字 1.this的类型&#xff1a;哪个对象调用就是哪个对象的引用类型 二、用法总结 1.this.data; //访问属性 2.this.func(); //访问方法 3.this(); //调用本类中其他构造方法 三、解释用法 1.this.data 这种是在成员方法中使用 让我们来看看不加this会出现什…

ArcGIS基础实验操作100例--实验95平滑处理栅格数据

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 空间分析篇--实验95 平滑处理栅格数据 目录 一、实验背景 二、实验数据 三、实验步骤 &#xff08;1&…