展开菜单
首页 精品内容 本月促销 装机必备 Windows macOS软件 IOS软件 Android AI PDF教程 专题
全部分类

当前位置:

首页 > 编程开发 > 响应式编程(反应式编程)的来龙去脉(同步编程、多线程编程、异步编程再到响应式编程)

响应式编程(反应式编程)的来龙去脉(同步编程、多线程编程、异步编程再到响应式编程)

从同步编程到多线程、异步编程,编程思维不断进化。响应式编程采用订阅-发布模型,将异步操作转化为数据流处理,通过声明式描述任务依赖关系,避免了回调地狱,提升了代码可读性和维护性。

响应式编程的来龙去脉(同步编程、多线程编程、异步编程再到响应式编程)

如果要讲清楚响应式编程的来龙去脉,最好从最基础的同步编程开始,一步步看到多线程、异步编程,最后再进入响应式编程的世界。这个过程本身就是编程思维不断进化的缩影。

简介

这篇文章会从头梳理这一脉络。从同步编程讲起,到多线程编程的出现,再到为了解决线程阻塞、资源浪费而诞生的异步编程,以及为了让异步编程更好写、更好维护而出现的响应式编程。

1. 示例

我们用同一个实际编程的例子来感受这几种编程方式的区别。

假设要执行一个任务A:先调用功能M算出一个结果,同时调用功能N算出另一个结果,然后把两个结果做某种计算,得到最终结果。M和N都需要一定的时间(比如有阻塞的IO操作)。

任务A示意图

接下来我们会用不同的编程模式来实现同样的功能,重点感受每种模式的特点。

2. 同步编程

最简单也最直接的思路:单线程、顺序执行。

代码写出来就是一步一步的:先通过功能M算出m,再通过功能N算出n,然后计算m+n。M和N各自需要1000ms的等待时间。

public static int functionM(){
    try {
        // calculate m...
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return 1;
}

public static int functionN(){
    try {
        // calculate n...
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return 2;
}

主线程执行如下:

public static void main(String[] args) throws InterruptedException {
    long start = System.currentTimeMillis();
    int m = functionM();
    int n = functionN();
    //Complex calculations
    Thread.sleep(1000);
    System.out.println("计算结果:"+(m+n));
    System.out.println("耗时:"+(System.currentTimeMillis()-start));
}

结果是:耗时3036ms,接近3秒。这就是单线程顺序执行的结果,M和N依次等待,浪费了大量时间。

3. 多线程编程

仔细观察任务A:M和N的执行其实是相互独立的,完全可以同时进行。于是我们想到了多线程并行。

任务A可以拆解成B、C、D三个子任务:B执行M,C执行N,D等待B和C的结果再进行计算。B和C之间没有依赖关系。

任务分解示意图

实现方式就是给B和C各自分配一个子线程,主线程通过Future等待结果,然后执行D。

public class MultithreadingDemo {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        long start = System.currentTimeMillis();
        ExecutorService threadpool = Executors.newCachedThreadPool();
        Future FutureTaskB = threadpool.submit(MultithreadingDemo::TaskB);
        Future FutureTaskC = threadpool.submit(MultithreadingDemo::TaskC);
        int m = FutureTaskB.get();
        int n = FutureTaskC.get();
        int result = TaskD(m, n);
        System.out.println("计算结果:" + result);
        System.out.println("耗时:" + (System.currentTimeMillis() - start));
        threadpool.shutdown();
    }
    public static int TaskB() { return functionM(); }
    public static int TaskC() { return functionN(); }
    public static int TaskD(int m, int n) throws InterruptedException {
        Thread.sleep(1000);
        return m + n;
    }
}

运行结果:耗时2078ms,比同步方式快了将近1秒。这就是多线程并行的效果。

4. 异步编程

多线程虽然解决了并行,但仔细看上面的代码:主线程在调用FutureTaskB.get()时是阻塞的——它一直在等子线程的结果,这段时间内什么都干不了。如果业务流量很高,线程是一种宝贵资源,大量线程这样空等着,很容易耗尽系统内存。

于是异步编程的思路出现了:主线程发出任务指令后直接返回,不等结果。等B和C执行完了,再安排其他线程(不一定是原来的线程)去执行后续操作。我们的关注点从“谁执行、什么时候执行”转移到“任务之间的依赖关系”上。

Ja va 8的CompletableFuture是一个很好的工具。我们用它来对这个过程建模:主线程只负责编排任务,然后就可以去做别的事情,不必等着。

任务依赖关系图:

任务依赖关系图

代码示例如下:

public static void main(String[] args) throws InterruptedException, ExecutionException {
    triggerA();
    //处理其他任务,完全不用管理任务A。
    Thread.sleep(3000);
}
public static void triggerA() {
    long start = System.currentTimeMillis();
    System.out.println("编排任务A线程ID为:"+Thread.currentThread().getId());
    CompletableFuture taskBFuture = CompletableFuture.supplyAsync(()->{
        System.out.println("执行任务B的当前线程ID:"+Thread.currentThread().getId());
        return taskB();
    });
    CompletableFuture taskCFuture = CompletableFuture.supplyAsync(()->{
        System.out.println("执行任务C的当前线程ID:"+Thread.currentThread().getId());
        return taskC();
    });
    //当任务C、D完成之后,回调执行任务D
    taskBFuture.thenCombineAsync(taskCFuture,(m,n)->{
        try {
            System.out.println("执行任务C的当前线程ID:"+Thread.currentThread().getId());
            Thread.sleep(1000);
            System.out.println("耗时:"+(System.currentTimeMillis()-start));
            System.out.println("taskA计算结果:"+(m+n));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    });
}
public static int taskB() { return functionM(); }
public static int taskC() { return functionN(); }

运行结果:耗时2042ms,线程1(主线程)编排完任务后就去忙别的了,B和C分别在11、12线程执行,然后由11线程执行D。主线程没有被阻塞。

这里还引出一个更直观的例子:C#的异步编程。三个任务,每个任务分为前、中、后三部分,中间部分是等待操作(如IO)。传统思路下线程会阻塞在等待操作上;异步编程则让线程直接返回,去执行其他任务,等等待完成了再派一个线程去执行后续操作。

C#异步执行结果示例

Ja va中的非阻塞IO也是类似思想:线程不等待IO操作,而是通过事件机制在IO完成后唤醒执行后续操作。这种模式在高并发场景下效果显著,极大解放了线程资源。

5. 响应式编程

5.1 响应式的由来

从上面的演变可以看到,编码的视角在逐渐从“控制任务执行”转向“描述任务本身和任务之间的依赖关系”。我们越来越关注“事情本身”。

但之前举的例子都很简单。如果业务逻辑复杂,任务的编排也会变得复杂、难以实现。这就是响应式编程要解决的问题。

回过头看我们拆解的任务B、C、D:B和C完成之后回调D执行。与其叫“回调”,不如叫“触发”——B、C的执行结束事件触发了D的执行。换句话说,我们可以看作D订阅了B、C的结束事件。当事件发生时,D开始响应并执行。

事件订阅发布示意图

发布者发布各种事件(包括BC执行结束的事件),订阅者订阅这些事件。BC执行结束后发布事件通知订阅者,订阅者响应执行D。这就是用订阅-发布模式来表现异步编程。

这么做的好处是:在响应式编程中,一系列事件(产生事件的地方)被看作流或管道。利用函数式编程中对流的处理,可以把异步编程中的许多操作转化为对数据流的操作,尤其是在对流进行转换时,效率和优雅度都会提升一个台阶。

拿Project Reactor举个例子。有一个管道每隔100ms发布一个数字(相当于事件),一个订阅者负责打印这些数字。传统异步操作肯定会阻塞等待,浪费线程资源。改用响应式编程:

Flux.range(1,10)
    .delayElements(Duration.of(100,ChronoUnit.MILLIS))
    .subscribe(System.out::println);

这段代码的意思是:声明一个可以生成1到10数字的管道(Flux.range(1,10)),每隔100ms生成一个数字(delayElements),打印服务订阅这个管道(subscribe),有元素生成时就通知打印服务进行打印(System.out::println)。

为了更直观地对比不同风格的异步编程,再看两个Project Reactor官方的例子。

5.2 回调 vs 响应式

需求:展示五个商品——优先展示用户最喜爱的五个商品,如果没有最喜爱商品,就用推荐的商品展示。

使用回调实现(可用Gua va实现):

userService.getFa vorites(userId, new Callback>() {
    public void onSuccess(List list) {
        if (list.isEmpty()) {
            suggestionService.getSuggestions(new Callback>() {
                public void onSuccess(List list) {
                    UiUtils.submitOnUiThread(() -> {
                        list.stream().limit(5).forEach(uiList::show);
                    });
                }
                public void onError(Throwable error) {
                    UiUtils.errorPopup(error);
                }
            });
        } else {
            list.stream()
                .limit(5)
                .forEach(fa vId -> fa voriteService.getDetails(fa vId, 
                    new Callback() {
                        public void onSuccess(Fa vorite details) {
                            UiUtils.submitOnUiThread(() -> uiList.show(details));
                        }
                        public void onError(Throwable error) {
                            UiUtils.errorPopup(error);
                        }
                    }));
        }
    }
    public void onError(Throwable error) {
        UiUtils.errorPopup(error);
    }
});

一层层的回调嵌套,对于开发和维护来说简直是地狱——这被称为“回调地狱”(Callback Hell)。命令式编程可读性极差,层层缩进,难以维护,容易出错。

使用响应式编程实现:

userService.getFa vorites(userId)
    .flatMap(fa voriteService::getDetails)
    .switchIfEmpty(suggestionService.getSuggestions())
    .take(5)
    .publishOn(UiUtils.uiThreadScheduler())
    .subscribe(uiList::show, UiUtils::errorPopup);

流程清晰,转换操作简单,底层异步实现被完全屏蔽。只需要把要做什么表达出来,描述好任务之间的关系,功能就实现了。

5.3 CompletableFuture 响应式

再看一个例子:用一个ID队列,分别获取每个ID的name和statistic,然后组合起来。这是典型的异步编排场景。

CompletableFuture实现:

CompletableFuture> ids = ifhIds();
CompletableFuture> result = ids.thenComposeAsync(l -> {
    Stream> zip = l.stream().map(i -> {
        CompletableFuture nameTask = ifhName(i);
        CompletableFuture statTask = ifhStat(i);
        return nameTask.thenCombineAsync(statTask, 
            (name, stat) -> "Name " + name + " has stats " + stat);
    });
    List> combinationList = zip.collect(Collectors.toList());
    CompletableFuture[] combinationArray = 
        combinationList.toArray(new CompletableFuture[combinationList.size()]);
    CompletableFuture allDone = CompletableFuture.allOf(combinationArray);
    return allDone.thenApply(v -> 
        combinationList.stream().map(CompletableFuture::join)
            .collect(Collectors.toList()));
});
List results = result.join();
assertThat(results).contains(...);

虽然比原生回调简化了一些,但读起来依然比较吃力。任务依赖关系图可以帮助理解:

CompletableFuture任务依赖图

响应式编程实现:

Flux ids = ifhrIds();
Flux combinations = ids.flatMap(id -> {
    Mono nameTask = ifhrName(id);
    Mono statTask = ifhrStat(id);
    return nameTask.zipWith(statTask, 
        (name, stat) -> "Name " + name + " has stats " + stat);
});
Mono> result = combinations.collectList();
List results = result.block();
assertThat(results).containsExactly(...);

FLUX是可以生成0到N个元素的管道,Mono是可以生成0到1个元素的管道。代码的声明性更强,对任务编排的API也更友好,开发和理解的难度明显降低。

从回调 → CompletableFuture → 响应式编程,代码的可维护性、声明性和面对复杂业务的灵活性都在逐步提高。

最后用响应式编程改造一下一开始那个异步编程的例子:

public class ReactiveProgramDemo {
    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        Mono monoB = Mono.fromCallable(ReactiveProgramDemo::taskB)
            .publishOn(Schedulers.boundedElastic());
        Mono monoC = Mono.fromCallable(ReactiveProgramDemo::taskC)
            .publishOn(Schedulers.boundedElastic());
        Mono.zip(monoB, monoC)
            .subscribeOn(Schedulers.boundedElastic())
            .subscribe(nums -> {
                System.out.println("计算结果为:" + taskD(nums.getT1(), nums.getT2()));
                System.out.println("tasA执行用时:" + (System.currentTimeMillis() - start));
            });
        //主线程做其他的事情
        Thread.sleep(5000);
    }
    // ... taskB, taskC, taskD 同上
}

结果:耗时2249ms,一切异步执行,主线程不被阻塞。

5.4 响应式编程的特性

随着响应式编程的普及,reactive-streams规范也逐渐成形,现在的响应式框架都是按照这个规范进行接口实现的。概括一下它的核心特性:

  • 异步编程的实现:响应式编程本质上就是一种异步编程,与基于回调、CompletableFuture实现的异步编程属于同一范畴。
  • 更好的表达性,不易出错:用声明式的方式描述业务逻辑,而不是控制流程,自然减少了出错概率。
  • 更灵活的操作方式:得益于数据流的函数式编程,提供了丰富的转换和操作符。
  • 更好的异常处理:其他方式的异步编程很难做到优雅的异常处理,响应式编程可以轻松实现。
Flux.range(1,100)
    .map(x->1/(x%10))
    .doOnNext(System.out::println)
    .doOnError(System.err::println)
    .subscribe()
  • 支持背压:可以控制上游管道的发送速度,避免下游消费不过来。

这些特性仅仅是入门,后续的文章会针对响应式编程进行更详细的讲解。

6. 总结

从最初的问题出发,我们经历了几个阶段:

先是同步编程——顺序执行,简单直接,但不能避免等待。

然后发现有些地方可以并行执行,于是出现了多线程编程,但依然没有摆脱“同步等待”的低效率问题。

随着非阻塞IO和异步接口(如Ja va的NIO和CompletableFuture)的普及,异步编程流行起来。它大大提高了线程的利用率,让服务在更少的时间内完成更多计算,性能大幅提升。

但新的问题随之而来:异步编程在面对复杂业务时的设计难度大,任务之间的关系复杂。用传统方式实现异步编程,容易出现冗长的嵌套代码,维护成本高,而且对复杂业务的任务编排十分困难。

这时候,封装更完善的响应式编程出现了。它从事件的角度出发,将异步编程转化为对事件流的操作,利用函数式编程对数据流进行处理,让异步编程变得更加优雅。同时提供了灵活的转换操作、异常处理和背压机制。对于异步编程来说,这确实是令人兴奋的进步。

7. 思考

语言或框架的接口提供了异步的能力,它们到底是怎么实现的?

8. 参考

一文带你彻底了解ja va异步编程
ja va-8-completablefuture-tutorial
Reactor 3 Reference Guide
Difference Between Asynchronous Programming and Multithreading in C#
What is the difference between asynchronous programming and multithreading?

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
作者最新文章
编程开发
上一篇: SHELL编程
相关文章 更多
精品专题 更多
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

macOS软件

正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

IOS软件

正软商城iOS软件专区,精选适用于iPhone和iPad的办公、学习、影音、设计、效率及AI应用,提供功能介绍、适用设备、系统要求和正版获取方式等信息。

AI

正软商城AI软件专区,汇集AI写作、AI绘画、AI视频、AI办公、AI编程、AI翻译、智能客服和数据分析等人工智能工具,提供功能介绍、适用平台、收费方式及正版购买信息。

Mac软件 更多
photoshop
photoshop

Photoshop 2026 是 Adobe 推出的专业图像处理与视觉设计软件,支持 Windows、macOS 和 iPad 等平台,广泛应用于摄影修图、电商设计、平面海报、数字绘画及视觉合成等创作场景。

Blender
Blender

Blender 是一款免费开源、跨平台的专业 3D 创作软件,集建模、动画、渲染、视频编辑与视觉合成等功能于一体,广泛应用于影视动画、游戏设计和建筑可视化等领域。软件支持 Cycles 物理渲染器与 Eevee 实时渲染引擎,并提供多边形建模、骨骼绑定、物理模拟等专业工具。Blender 兼容 Windows、macOS 和 Linux 系统,安装包轻巧、运行流畅,依托活跃的全球开发者社区持续更新,是从初学者到专业创作者都值得选择的正版 3D 创作工具。

灵活计算器
灵活计算器

灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

赤友清理大师
赤友清理大师

赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

极度公式
极度公式

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

图几
图几

图几是一款适用于 macOS 的截图、标注与美化工具,支持离线操作保障隐私。界面整理和高频系统操作被放到一起考虑,桌面或窗口内容一多时,管理起来会更省心。

密码键盘
密码键盘

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。

思源笔记
思源笔记

思源笔记是一款本地笔记软件,提供所见即所得的编辑方式,为长文写作带来顺滑的体验。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

Office 365 简体中文
Office 365 简体中文

一款文字处理软件,一种订阅式的跨平台办公软件,基于云平台提供多种服务,通过将 Excel 和 Outlook 等应用与 OneDrive 和 Microsoft Teams 等强大的云服务相结合,Office 365 可让任何人使用任何设备随时随地创建和共享内容。

Mac
WALTR PRO
WALTR PRO

WALTR是一款电脑至iOS文件传输转换工具,操作简单,快速实现文件识别与传送。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

CodeExpander
CodeExpander

CodeExpander 是一款快捷短语输入增强工具,通过键入缩写自动展开为自定义文段,提升工作效率。任务管理和过程控制会更完整,持续下载、批量同步或需要稳定传输流程的场景会更适合它。

Mountain Duck
Mountain Duck

Mountain Duck 是一款能将多个网盘挂载到本地的工具,像本地磁盘一样使用网盘。清理链路的完整性会更好一些,做应用卸载、残留处理和空间整理时,通常能少走很多手动排查步骤。

WINDOWS 更多
3dmax(3ds max)
3dmax(3ds max)

Autodesk 3ds Max 是一款专业的三维建模、动画与渲染软件,广泛应用于建筑可视化、游戏开发、影视动画、广告设计和产品展示等领域。

photoshop
photoshop

Photoshop 2026 是 Adobe 推出的专业图像处理与视觉设计软件,支持 Windows、macOS 和 iPad 等平台,广泛应用于摄影修图、电商设计、平面海报、数字绘画及视觉合成等创作场景。

Blender
Blender

Blender 是一款免费开源、跨平台的专业 3D 创作软件,集建模、动画、渲染、视频编辑与视觉合成等功能于一体,广泛应用于影视动画、游戏设计和建筑可视化等领域。软件支持 Cycles 物理渲染器与 Eevee 实时渲染引擎,并提供多边形建模、骨骼绑定、物理模拟等专业工具。Blender 兼容 Windows、macOS 和 Linux 系统,安装包轻巧、运行流畅,依托活跃的全球开发者社区持续更新,是从初学者到专业创作者都值得选择的正版 3D 创作工具。

Windows 10
Windows 10

Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

极度公式
极度公式

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

密码键盘
密码键盘

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。

思源笔记
思源笔记

思源笔记是一款本地笔记软件,提供所见即所得的编辑方式,为长文写作带来顺滑的体验。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

傲梅轻松备份
傲梅轻松备份

傲梅轻松备份是一款专业易用的数据备份软件,为重要数据提供安全保障。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。

Office 365 简体中文
Office 365 简体中文

一款文字处理软件,一种订阅式的跨平台办公软件,基于云平台提供多种服务,通过将 Excel 和 Outlook 等应用与 OneDrive 和 Microsoft Teams 等强大的云服务相结合,Office 365 可让任何人使用任何设备随时随地创建和共享内容。

Mac
Wise Folder Hider Pro
Wise Folder Hider Pro

Wise Folder Hider Pro 是一款专业级文件和文件夹隐藏加密软件,为私密数据添加多重保护。高频操作更强调就近处理,浏览、整理和跨目录移动文件时,来回切换和重复点击都会少很多。

WALTR PRO
WALTR PRO

WALTR是一款电脑至iOS文件传输转换工具,操作简单,快速实现文件识别与传送。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

CodeExpander
CodeExpander

CodeExpander 是一款快捷短语输入增强工具,通过键入缩写自动展开为自定义文段,提升工作效率。任务管理和过程控制会更完整,持续下载、批量同步或需要稳定传输流程的场景会更适合它。