响应式编程与协程

响应式编程与协程的比较

  • 响应式编程的弊端
  • 虚拟线程
    • Java线程
    • 内核线程的局限性
    • 传统线程池的demo
    • 虚拟线程的demo

响应式编程的弊端

前面用了几篇文章介绍了响应式编程,它更多的使用少量线程实现线程间解耦和异步的作用,如线程的Reactor模型,主要是把接收请求交给IO线程处理,然后业务的处理交给handler线程处理,它的弊端是①开发编码成本比较高,如下例demo:

Flux<Object> fluxDemo = Flux.create(sink -> {for (int i = 0; i < 3; i++) {sink.next(i);}//代表推送给下一个操作形成新流sink.complete();//过滤不等于0的数
}).log().filter(Predicate.not(Predicate.isEqual(0))).publishOn(Schedulers.single()).log();

发布者往下推送,相应于不断的回调,虽然说webFlux组件也尽量避免回调地狱,以及命令式编程的复杂性,②仍有使用函数式编程和事件驱动存在理解性上的难度,基于现有代码去改造,成本比较高,③响应式编程需要底层的很多第三方库支持,而这种第三方库是比不上JDK官方版本的代码质量,这三个原因是webflux没有大范围被应用的原因;

而且从线程角度,虽然响应式编程使用少量线程处理,在handler处理业务,也就是用户线程进行处理,如线程阻塞,cpu就需将调度切换到另一个线程,但仍然存在上下文切换的问题:

  1. 寄存器保存与恢复 线程切换时,操作系统需要保存当前线程的寄存器状态(如程序计数器、堆栈指针等),并恢复新线程的寄存器状态。这些操作涉及大量内存访问,增加了时间开销。

  2. 缓存失效 线程切换可能导致CPU缓存失效,新线程的数据和指令可能不在缓存中,需要从主存加载,这会显著增加延迟。

  3. 内存管理 切换线程时,操作系统可能需要更新内存管理单元(MMU)的页表,确保新线程能正确访问其内存空间。这一过程涉及TLB(转换后备缓冲区)的刷新,进一步增加延迟。

  4. 内核态与用户态切换 线程切换通常需要从用户态切换到内核态,执行完后再切换回用户态。这种模式切换涉及额外的开销。

  5. 调度开销 操作系统需要选择下一个要执行的线程,调度算法的复杂性也会影响切换速度,尤其是在高负载情况下。

  6. 锁与同步 在多线程环境中,切换可能涉及锁的获取与释放,若锁被其他线程持有,当前线程会被阻塞,进一步增加延迟。

  7. 中断处理 硬件中断可能触发线程切换,操作系统需要先处理中断,再执行切换,增加了额外开销。

  8. 上下文大小 线程的上下文越大,保存和恢复所需的时间越长,尤其是在寄存器多或内存占用大的情况下。

恰好JDK引入虚拟线程,从另外角度去解决并发问题。响应式编程和虚拟线程是竞品,在CPU密集型的业务场景中,响应式编程吞吐量是由于虚拟线程的,但在IO密集型中,虚拟线程吞吐量要高一些,所以与虚拟线程对比,spring webflux是弊大于利的,这也是响应式编程一直没有流行开来的原因;

虚拟线程

虚拟线程在Java 19中以预览模式引入,并在Java 21版本中正式成为标准功能,Java的虚拟线程参考了Golang这种协程的机制;

Java线程

内核线程直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统内就有能力同时处理多件事,但程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程,轻量级进程就是通常意义上说的线程

每个轻量级进程都称为一个独立的调度单元,它是基于内核线程实现的,所以创建、析构和同步,都需要进行系统调用,前面也说系统调用是需要用户态切换到内核态的

其实在JDK1.2之前,Java线程就基于一种被称为“绿色线程”的用户线程实现,但从JDK1.3起,“主流”平台上的“主流”商用Java虚拟机的线程模型普遍都被换成基于操作系统原生线程模型来实现,即采用1:1的线程模型,以HotSpot为例,它的每一个Java线程都是直接映射到一个操作系统原生线程(就是内核线程)来实现的,而且中间没有额外的间接结构,所以HotSpot自己是不会去干涉线程调度的(但可以设置线程优先级给操作系统提供调度建议),全权交给底下的操作系统去处理,所以何时冻结或唤醒线程、该给线程分配多少处理器执行时间、该把线程安排给那个处理器核心去执行等,都是由操作系统完成的,也都是由操作系统全权决定的,例如只有cpu只有8个逻辑核,它就是创建8个原生线程,无论创建的线程池有多少个用户线程,都是调用轻量级进程接口让cpu切换着执行(至于cpu调度可参考之前的白话讲Linux进程如何被CPU调度)

内核线程的局限性

随着业务量的增加,QPS也要求越来越大,而Web应用的服务却要求每个接口的吞吐量保持大,这就要求每个服务都必须在极短时间内完成计算,1:1的内核线程模型是如今虚拟机线程实现的主流选择,但是这种映射到操作系统上的线程天然的缺陷是切换、调度成本高昂,系统能容纳的线程数量也是有限的;
用户线程的上下文切换是一种重量级的操作(上面有说上下文切换操作慢的原因),每遇到IO阻塞,就需要切换上下文,以及如果进行量化的话,那么如果不显示设置-Xss或-XX:ThreadStackSize,则在64位Linux上HotSpot的线程栈容量默认是1M,此外内核数据结构还额外消耗16Kb内存。

所以引入虚拟线程,也叫协程,它分为有栈和无栈协程序,通过在内存划分一片额外空间来模拟调用栈,只要其他“线程”中方法压栈、退栈时遵守规则,不破坏这片空间即可,这样多段代码执行时就会像相互缠绕着一样,非常形象。后来,操作系统开始提供多线程的支持,靠应用自己模拟多线程的做法自然是变少许多,而是演化为用户线程继续存在,也就说虚拟线程是在用户线程的基础上创建的,无论是创建和销毁都无需切换到内核态,性能自然高,而且一个协程的栈通常在几百个字节到几KB之间,所以Java虚拟机里线程池容量达到两百就已不小了,而支持协程的应用中,同时存在的协程数量可数以十万计;

传统线程池的demo

static class Task implements Runnable{CountDownLatch countDownLatch = null;Task(CountDownLatch countDownLatch){this.countDownLatch = countDownLatch;}@Overridepublic void run() {System.out.println(Thread.currentThread()+":开始");System.out.println(Thread.currentThread()+":虚拟线程在执行");try {Thread.sleep(1000);countDownLatch.countDown();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread()+":结束");}}
public static void main(String[] args) throws IOException, InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(3);ExecutorService executorService = Executors.newFixedThreadPool(1);long before = System.currentTimeMillis();executorService.execute(new Task(countDownLatch));executorService.execute(new Task(countDownLatch));executorService.execute(new Task(countDownLatch));countDownLatch.await();long after = System.currentTimeMillis() - before;System.out.println("耗费时间为"+after);System.in.read();
}

在这里插入图片描述
该demo中创建的线程池只有一个,提交的三个任务串行执行,耗费时间是三个任务执行时间总和;

虚拟线程的demo

public static void main(String[] args) throws IOException, InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(3);ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();long before = System.currentTimeMillis();executorService.execute(new Task(countDownLatch));executorService.execute(new Task(countDownLatch));executorService.execute(new Task(countDownLatch));long after = System.currentTimeMillis() - before;System.out.println("耗费时间为"+after);System.in.read();
}

在这里插入图片描述
创建虚拟线程,还是执行上面同一个Task任务,可以看到打印的线程名称都是ForkJoinPool-1,worker1、2、3共三个,也就是只创建了一个线程,在该线程的基础上创建了三个虚拟线程,执行时间不再是串行的3s,只是8ms;
可见对于IO密集型的任务,创建虚拟线程不仅可节省大量线程的内存,还有提高效率;

如有需要收藏的看官,顺便也用发财的小手点点赞哈,如有错漏,也欢迎各位在评论区评论!

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

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

相关文章

python学opencv|读取图像(五十三)原理探索:使用cv.matchTemplate()函数实现最佳图像匹配

【1】引言 前序学习进程中&#xff0c;已经探索了使用cv.matchTemplate()函数实现最佳图像匹配的技巧&#xff0c;并且成功对两个目标进行了匹配。 相关文章链接为&#xff1a;python学opencv|读取图像&#xff08;五十二&#xff09;使用cv.matchTemplate()函数实现最佳图像…

javaEE-8.JVM(八股文系列)

目录 一.简介 二.JVM中的内存划分 JVM的内存划分图: 堆区:​编辑 栈区:​编辑 程序计数器&#xff1a;​编辑 元数据区&#xff1a;​编辑 经典笔试题&#xff1a; 三,JVM的类加载机制 1.加载: 2.验证: 3.准备: 4.解析: 5.初始化: 双亲委派模型 概念: JVM的类加…

【01】共识机制

BTF共识 拜占庭将军问题 拜占庭将军问题是一个共识问题 起源 Leslie Lamport在论文《The Byzantine Generals Problem》提出拜占庭将军问题。 核心描述 军中可能有叛徒&#xff0c;却要保证进攻一致&#xff0c;由此引申到计算领域&#xff0c;发展成了一种容错理论。随着…

AI大模型开发原理篇-1:语言模型雏形之N-Gram模型

N-Gram模型概念 N-Gram模型是一种基于统计的语言模型&#xff0c;用于预测文本中某个词语的出现概率。它通过分析一个词语序列中前面N-1个词的出现频率来预测下一个词的出现。具体来说&#xff0c;N-Gram模型通过将文本切分为长度为N的词序列来进行建模。 注意&#xff1a;这…

【汽车电子软件架构】AutoSAR从放弃到入门专栏导读

本文是汽车电子软件架构&#xff1a;AutoSAR从放弃到入门专栏的导读篇。文章延续专栏文章的一贯作风&#xff0c;从概念与定义入手&#xff0c;希望读者能对AutoSAR架构有一个整体的认识&#xff0c;然后对专栏涉及的文章进行分类与链接。本文首先从AutoSAR汽车软件架构的概念&…

python-UnitTest框架笔记

UnitTest框架的基本使用方法 UnitTest框架介绍 框架&#xff1a;framework&#xff0c;为了解决一类事情的功能集合 UnitTest框架&#xff1a;是python自带的单元测试框架 自带的&#xff0c;可以直接使用&#xff0c;不需要格外安装 测试人员用来做自动化测试&#xff0c;作…

【数据结构】_链表经典算法OJ:复杂链表的复制

目录 1. 题目链接及描述 2. 解题思路 3. 程序 1. 题目链接及描述 题目链接&#xff1a;138. 随机链表的复制 - 力扣&#xff08;LeetCode&#xff09; 题目描述&#xff1a; 给你一个长度为 n 的链表&#xff0c;每个节点包含一个额外增加的随机指针 random &#xff0c;…

Linux——进程间通信之SystemV共享内存

前言 SystemV通信一般包括三种&#xff1a;共享内存、消息队列和信号量。共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间&#xff0c;这些进程间数据传递不再涉及到 内核&#xff0c;换句话说是进程不再通过执行进入内核的系统调用来…

Linux网络 | 网络层IP报文解析、认识网段划分与IP地址

前言&#xff1a;本节内容为网络层。 主要讲解IP协议报文字段以及分离有效载荷。 另外&#xff0c; 本节也会带领友友认识一下IP地址的划分。 那么现在废话不多说&#xff0c; 开始我们的学习吧&#xff01;&#xff01; ps&#xff1a;本节正式进入网络层喽&#xff0c; 友友们…

SQLGlot:用SQLGlot解析SQL

几十年来&#xff0c;结构化查询语言&#xff08;SQL&#xff09;一直是与数据库交互的实际语言。在一段时间内&#xff0c;不同的数据库在支持通用SQL语法的同时演变出了不同的SQL风格&#xff0c;也就是方言。这可能是SQL被广泛采用和流行的原因之一。 SQL解析是解构SQL查询…

Windows程序设计10:文件指针及目录的创建与删除

文章目录 前言一、文件指针是什么&#xff1f;二、设置文件指针的位置&#xff1a;随机读写&#xff0c;SetFilePointer函数1.函数说明2.函数实例 三、 目录的创建CreateDirectory四、目录的删除RemoveDirectory总结 前言 Windows程序设计10&#xff1a;文件指针及目录的创建与…

线程互斥同步

前言&#xff1a; 简单回顾一下上文所学&#xff0c;上文我们最重要核心的工作就是介绍了我们线程自己的LWP和tid究竟是个什么&#xff0c;总结一句话&#xff0c;就是tid是用户视角下所认为的概念&#xff0c;因为在Linux系统中&#xff0c;从来没有线程这一说法&#xff0c;…

DRM系列七:Drm之CREATE_DUMB

本系列文章基于linux 5.15 DRM驱动的显存由GEM&#xff08;Graphics execution management&#xff09;管理。 一、创建流程 创建buf时&#xff0c;user层提供需要buf的width,height以及bpp(bite per pixel)&#xff0c;然后调用drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &…

我们信仰AI?从神明到人工智能——信任的进化

信任的进化&#xff1a; 信任是我们最宝贵的资产。而现在&#xff0c;它正像黑色星期五促销的廉价平板电视一样&#xff0c;被一点点拆解。在过去&#xff0c;世界很简单&#xff1a;人们相信晚间新闻、那些满是灰尘书籍的教授&#xff0c;或者手持病历、眉头紧锁的医生。而如…

数据分析系列--[11] RapidMiner,K-Means聚类分析(含数据集)

一、数据集 二、导入数据 三、K-Means聚类 数据说明:提供一组数据,含体重、胆固醇、性别。 分析目标:找到这组数据中需要治疗的群体供后续使用。 一、数据集 点击下载数据集 二、导入数据 三、K-Means聚类 Ending, congratulations, youre done.

1-刷力扣问题记录

25.1.19 1.size()和.length()有什么区别 2.result.push_back({nums[i], nums[left], nums[right]});为什么用大括号&#xff1f; 使用大括号 {} 是 C11 引入的 初始化列表 语法&#xff0c;它允许我们在构造或初始化对象时直接传入一组值。大括号的使用在许多情况下都能让代码…

神经网络参数量和运算量的计算- 基于deepspeed库和thop库函数

引言 最近需要对神经网络的参数量和运算量进行统计。找到一个基于deepspeed库函数计算参数量和运算量的例子。而我之前一直用thop库函数来计算。 看到有一篇勘误博文写道使用thops库得到的运算量是MACs (Multiply ACcumulate operations&#xff0c;乘加累积操作次数&#xf…

读书笔记--分布式架构的异步化和缓存技术原理及应用场景

本篇是在上一篇的基础上&#xff0c;主要对分布式应用架构下的异步化机制和缓存技术进行学习&#xff0c;主要记录和思考如下&#xff0c;供大家学习参考。大家知道原来传统的单一WAR应用中&#xff0c;由于所有数据都在同一个数据库中&#xff0c;因此事务问题一般借助数据库事…

无用知识研究:std::initializer_list的秘密

先说结论&#xff0c;用std::initializer_list初始化vector&#xff0c;内部逻辑是先生成了一个临时数组&#xff0c;进行了拷贝构造&#xff0c;然后用这个数组的起终指针初始化initializer_list。然后再用initializer_list对vector进行初始化&#xff0c;这个动作又触发了拷贝…

Jupyterlab和notebook修改文件的默认存放路径的方法

文章目录 1.缘由2.操作流程2.1找到默认的路径2.2创建配置文件2.3修改配置文件内容2.4注意事项 1.缘由 我自己使用jupyterlab的时候&#xff0c;打开是在这个浏览器上面打开的&#xff0c;但是这个打开的文件路径显示的是C盘上面路径&#xff0c;所以这个就很麻烦&#xff0c;因…