Java之协程(quasar)

  一、前面我们简单的说了一下,Python中的协程原理。这里补充Java的协程实现过程。有需要可以查看python之协程

  二、Java协程,其实做Java这么久我也没有怎么听过Java协程的东西,但是一直有有听到微线程/协程的概念,这不在学习Python的时候接触到了协程一词。然后返回来去了解Java的协程问题,但是看了很多资料,发现官网以及很多地方都没有涉及到协程的东西,没有办法,只能通过强大的社区来学习协程的相关东西。

  三、这里主要关注的是:quasar。

  1)协程的目的:当我们在使用多线程的时候,如果存在长时间的I/O操作。这个时候线程一直处于阻塞状态,如果线程很多的时候,会存在很多线程处于空闲状态,造成了资源应用不彻底。相对的协程不一样了,在单线程中多个任务来回自行如果出现长时间的I/O操作,让其让出目前的协程调度,执行下一个任务。当然可能所有任务,全部卡在同一个点上,但是这只是针对于单线程而言,当所有数据正常返回时,会同时处理当前的I/O操作。

  2)多线程测试(这里使用100万个线程,来测试内存占用) 

        for (int i = 0; i < 1000000; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }    

  结果:

  直接卡死了,内存溢出

  

  可想而知,如果存在100万个线程,开销是有多大。

  3)协程测试

  a、阿里云搜索到的依赖包

     <dependency>
            <groupId>co.paralleluniverse</groupId>
            <artifactId>quasar-core</artifactId>
            <version>0.7.9</version>
            <classifier>jdk8</classifier>
        </dependency>

  b、测试内存占用量

  public static void main(String[] args) throws Exception {
        //使用阻塞队列来获取结果。
        LinkedBlockingQueue<Fiber<Integer>> fiberQueue = new LinkedBlockingQueue<>();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        for (int i = 0; i < 1000000; i++) {
            int finalI = i;
            //这里的Fiber有点像Callable,可以返回数据
            Fiber<Integer> fiber = new Fiber<>((SuspendableCallable<Integer>) () -> {
                //这里用于测试内存占用量
                Fiber.sleep(100000);
                System.out.println("in-" + finalI + "-" + LocalDateTime.now().format(formatter));
                return finalI;
            });
            //开始执行
            fiber.start();
            //加入队列
            fiberQueue.add(fiber);
        }
        while (true) {
            //阻塞
            Fiber<Integer> fiber = fiberQueue.take();
            System.out.println("out-" + fiber.get() + "-" + LocalDateTime.now().format(formatter));
        }
    }

  结果:

  堆:

  

  估计:1个G左右。

  内存:

  

  估计:1个G左右,也就是每一个fiber占用1Kb左右。

  c、正常测试

  修改一下参数:

    public static void main(String[] args) throws Exception {
        //使用阻塞队列来获取结果。
        LinkedBlockingQueue<Fiber<Integer>> fiberQueue = new LinkedBlockingQueue<>();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            //这里的Fiber有点像Callable,可以返回数据
            Fiber<Integer> fiber = new Fiber<>((SuspendableCallable<Integer>) () -> {
                //这里用于测试内存占用量
                Fiber.sleep(1000);
                System.out.println("in-" + finalI + "-" + LocalDateTime.now().format(formatter));
                return finalI;
            });
            //开始执行
            fiber.start();
            //加入队列
            fiberQueue.add(fiber);
        }
        while (true) {
            //阻塞
            Fiber<Integer> fiber = fiberQueue.take();
            System.out.println("out-" + fiber.get() + "-" + LocalDateTime.now().format(formatter));
        }
    }

  结果:

  

  4)可以看出并发的状态还是很不错的,当然这只是多个任务执行而已。

  四、通过上面的测试,可以看出,quasar中Fiber,很像Callable的用法、而且在内存占用上面减少了很多,当然,堆的数量确实不少,但是可以接受。

  还是要说明:协程的方式更多用来做I/O密集型的操作。计算密集型的还是使用线程更加合理。

  五、原理,我估么着看了一下源码,复杂度很高,这里不做深究。

  原理参考:次时代Java编程(一):Java里的协程

  1、Quasar里的Fiber其实是一个continuation,他可以被Quasar定义的scheduler调度,一个continuation记录着运行实例的状态,而且会被随时中断,并且也会随后在他被中断的地方恢复。Quasar其实是通过修改bytecode来达到这个目的,所以运行Quasar程序的时候,你需要先通过java-agent在运行时修改你的代码,当然也可以在编译期间这么干。golang的内置了自己的调度器,Quasar则默认使用ForkJoinPool这个JDK7以后才有的,具有work-stealing功能的线程池来当调度器。work-stealing非常重要,因为你不清楚哪个Fiber会先执行完,而work-stealing可以动态的从其他的等等队列偷一个context过来,这样可以最大化使用CPU资源。

  2、那这里你会问了,Quasar怎么知道修改哪些字节码呢,其实也很简单,Quasar会通过java-agent在运行时扫描哪些方法是可以中断的,同时会在方法被调用前和调度后的方法内插入一些continuation逻辑,如果你在方法上定义了@Suspendable注解,那Quasar会对调用该注解的方法做类似下面的事情。

  3、这里假设你在方法f上定义了@Suspendable,同时去调用了有同样注解的方法g,那么所有调用f的方法会插入一些字节码,这些字节码的逻辑就是记录当前Fiber栈上的状态,以便在未来可以动态的恢复。(Fiber类似线程也有自己的栈)。在suspendable方法链内Fiber的父类会调用Fiber.park,这样会抛出SuspendExecution异常,从而来停止线程的运行,好让Quasar的调度器执行调度。这里的SuspendExecution会被Fiber自己捕获,业务层面上不应该捕获到。如果Fiber被唤醒了(调度器层面会去调用Fiber.unpark),那么f会在被中断的地方重新被调用(这里Fiber会知道自己在哪里被中断),同时会把g的调用结果(g会return结果)插入到f的恢复点,这样看上去就好像g的return是f的local variables了,从而避免了callback嵌套。

  4、上面啰嗦了一大堆,其实简单点讲就是,想办法让运行中的线程栈停下来,好让Quasar的调度器介入。JVM线程中断的条件只有两个,一个是抛异常,另外一个就是return。这里Quasar就是通过抛异常的方式来达到的,所以你会看到我上面的代码会抛出SuspendExecution。但是如果你真捕获到这个异常,那就说明有问题了,所以一般会这么写。

原文地址:https://www.cnblogs.com/ll409546297/p/10945119.html

时间: 2024-11-10 01:36:20

Java之协程(quasar)的相关文章

协程是什么

以下是我自己的理解: 一般的线程切换是由操作系统来执行的, 而协程则是一种特殊的线程,这种线程的切换是由用户自己来决定的,并且切换需要做的额外工作如:执行状态和执行位置的保存,也是由用户自己来做的. 以下是一个解释的比较清楚的内容: 笔者最美好的记忆来自于早年在6502 cpu的cc800上写汇编的年代, 那个时代的计算机甚至没有操作系统,也没有实模式等保护机制.在6502上写汇编应用其实非常简单,系统会把bin文件加载到一个固定的内存地址中,cpu会固定地从一个特定的位置开始执行.然后cpu就

Java的纤程库 - Quasar

最近遇到的一个问题大概是微服务架构中经常会遇到的一个问题: 服务 A 是我们开发的系统,它的业务需要调用 B . C . D 等多个服务,这些服务是通过http的访问提供的. 问题是 B . C . D 这些服务都是第三方提供的,不能保证它们的响应时间,快的话十几毫秒,慢的话甚至1秒多,所以这些服务的Latency比较长.幸运地是这些服务都是集群部署的,容错率和并发支持都比较高,所以不担心它们的并发性能,唯一不爽的就是就是它们的Latency太高了. 系统A会从Client接收Request,

Kotlin 协程真的比 Java 线程更高效吗?

本文首发于 vivo互联网技术 微信公众号? 链接:https://mp.weixin.qq.com/s/-OcCDI4L5GR8vVXSYhXJ7w 作者:吴越 网上几乎全部介绍Kotlin的文章都会说Kotlin的协程是多么的高效,比线程性能好很多,然而事情的真相真是如此么? 协程的概念本身并不新鲜,使用C++加上内嵌汇编,一个基本的协程模型50行代码之内就可以完全搞出来.早在2013年国内就有团队开源了号称支持千万并发的C++协程库 libco. 最近几年协程的概念越来越深入人心,主要还是

go协程goroutine与Java多线程比较

引言: 个人理解的线程,协程和单,多核线程 1. 单核CPU上运行的多线程程序, 同一时间只能一个线程在跑, 系统帮你切换线程而已(cpu时间切片), 系统给每个线程分配时间片来执行, 每个时间片大概10ms左右, 看起来像是同时跑, 但实际上是每个线程跑一点点就换到其它线程继续跑,效率不会有提高的,切换线程反倒会增加开销(线程的上下文切换),宏观的可看着并行,单核里面只是并发,真正执行的一个cpu核心只在同一时刻执行一个线程(不是进程). 2. 多线程的用处在于,做某个耗时的操作时,需要等待返

Java协程框架-Kilim字节码剖析

前面几篇文章从代码层面介绍了Kilim的基本原理,但是对于其中的一些细节,比如Task的执行状态如何管理等问题从代码上依然得不到答案,本文即再深入到字节码层面来解答. 1.  Kilim字节码改写前后的代码有什么区别? 这里还是先上Kilim官方文档中的一张图,这张图清晰的展现出原始的代码与经Kilim改写后的协程代码. 可以看出左边的原始代码,与我们常见的函数相比有所不同,这里显示声明抛出Pausable异常.实际上这个异常在运行期间不会抛出,它的实际作用类似于注解,使得Kilim能够识别哪些

Java协程框架--Kilim常见问题解答

1.Kilim中的Task,即用户线程如何调度和切换? 在多任务的调度上操作系统存在抢占式和协作式两种方式,相比传统的Thread多线程间抢占式调度,Kilim中的Task采用的是协作式调度,即由Task本身负责释放和恢复占用CPU. 2.Kilim如何识别代码中哪些方法是Pauseable,可暂停的? 通过Kilim提供的Weaver工具在代码编译后,对编译生成的字节码进行分析,识别哪些方法抛出Pauseable异常的,来判断识别方法可暂停的. 3.Kilim是如何实现线程执行过程中Task的

对协程的一些理解

协程协程(coroutine)最早由Melvin Conway在1963年提出并实现,一句话定义:协程是用户态的轻量级的线程 线程和协程线程和协程经常被放在一起比较:线程一旦被创建出来,编写者是无法决定什么时候获得或者放出时间片的,是由操作系统进行统一调度的:而协程对编写者来说是可以控制切换的时机,并且切换代价比线程小,因为不需要进行内核态的切换.协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力, 但是可用通过多个(进

Gevent的socket协程安全性分析

一般讨论socket的并发安全性,都是指线程的安全性...而且绝大多数的情况下socket都不是线程安全的.. 当然一些框架可能会对socket进行一层封装,让其成为线程安全的...例如java的netty框架就是如此,将socket封装成channel,然后让channel封闭到一个线程中,那么这个channel的所有的读写都在它所在的线程中串行的进行,那么自然也就是线程安全的了..... 其实很早看Gevent的源码的时候,就已经看过这部分的东西了,当时就已经知道gevent的socket不

【Python之路Day11】网络篇之线程、进程、协程

目录: 基本概念 线程 进程 协程  一. 基本概念 现在的操作系统,如Unix.Linux.Windows.Mac OS X等,都是支持“多任务”的操作系统. 什么叫”多任务“呢?简单理解,就是我们可以一般上网浏览某车之家的网页,看看喜欢的车型信息:一边打开某易云音乐听听好歌:一边打开某软件股市行情图,不安的盯着曲线图...卧槽,又尼玛跌了!  这就是多任务喽. 多核心的CPU已经很普及了,但是,就是在过去的单核心CPU上,也可以执行多任务. PS: CPU是分时间片的,假设任务1执行0.01