惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
人人都是产品经理
人人都是产品经理
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
V2EX
博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
D
Docker
S
SegmentFault 最新的问题
博客园 - 聂微东
美团技术团队
Apple Machine Learning Research
Apple Machine Learning Research
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Last Week in AI
Last Week in AI
M
MIT News - Artificial intelligence
F
Fortinet All Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
GbyAI
GbyAI
L
LangChain Blog
Vercel News
Vercel News
博客园 - 叶小钗
MongoDB | Blog
MongoDB | Blog
Stack Overflow Blog
Stack Overflow Blog
H
Help Net Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
Engineering at Meta
Engineering at Meta
T
Threat Research - Cisco Blogs
T
Threatpost
Scott Helme
Scott Helme
T
Tailwind CSS Blog
Latest news
Latest news
Stack Overflow Blog
Stack Overflow Blog
Blog — PlanetScale
Blog — PlanetScale
The Register - Security
The Register - Security
罗磊的独立博客
P
Proofpoint News Feed
腾讯CDC
S
Schneier on Security
雷峰网
雷峰网
A
About on SuperTechFans
T
Tenable Blog
F
Full Disclosure
Cyberwarzone
Cyberwarzone
博客园_首页
有赞技术团队
有赞技术团队
K
Kaspersky official blog

文章列表

设计模式-备忘录模式 - OXOXTECH 牛牛技术客栈 设计模式-中介者模式 - OXOXTECH 牛牛技术客栈 Linux【Ubuntu】修改ssh默认端口 - OXOXTECH 牛牛技术客栈 设计模式-迭代器模式 - OXOXTECH 牛牛技术客栈 scheduled定时任务的三种基本实现方式 - OXOXTECH 牛牛技术客栈 Apriori - 基于关联规则的推荐算法(三) - OXOXTECH 牛牛技术客栈 Apriori - 基于关联规则的推荐算法(二) - OXOXTECH 牛牛技术客栈 Apriori - 基于关联规则的推荐算法(一) - OXOXTECH 牛牛技术客栈 基于JavaFX的桌面端网络调试工具 - OXOXTECH 牛牛技术客栈 Golang Channel的原理介绍 - OXOXTECH 牛牛技术客栈 Go语言Map的原理分析 - OXOXTECH 牛牛技术客栈 Go语言错误处理(panic)的最佳实践 - OXOXTECH 牛牛技术客栈 设计模式-解释器模式 - OXOXTECH 牛牛技术客栈 Redis报错Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. go-webpbin库在Linux报错failed to encode image to WebP: exit status 1.......的问题 exe4j 打包加密的jar - OXOXTECH 牛牛技术客栈 Go生成图形验证码示例 - OXOXTECH 牛牛技术客栈 澳门一天游:一日尽享东方与西方的交融之美 - OXOXTECH 牛牛技术客栈 设计模式-命令模式 - OXOXTECH 牛牛技术客栈 别再自己瞎写工具类了,SpringBoot内置工具类应有尽有 - OXOXTECH 牛牛技术客栈 中山一日游 - OXOXTECH 牛牛技术客栈 设计模式-责任链模式 - OXOXTECH 牛牛技术客栈 起舞吧,齐舞吧 - OXOXTECH 牛牛技术客栈 设计模式-组合模式 - OXOXTECH 牛牛技术客栈 Go语言Web开发|GoFrame框架入门笔记 - OXOXTECH 牛牛技术客栈 Java打包exe教程 - OXOXTECH 牛牛技术客栈 设计模式-代理模式 - OXOXTECH 牛牛技术客栈 MySQL存储过程的优缺点有哪些? - OXOXTECH 牛牛技术客栈 前端渲染优化有哪些? - OXOXTECH 牛牛技术客栈 HTTP状态码及其含义 - OXOXTECH 牛牛技术客栈 从浏览器地址栏输入url到显示页面的步骤 - OXOXTECH 牛牛技术客栈 TypeScript事件派发管理器 - OXOXTECH 牛牛技术客栈 MQTT保留消息的使用方法 - OXOXTECH 牛牛技术客栈 世界工程-港珠澳大桥游 - OXOXTECH 牛牛技术客栈 Golang逃逸分析 - OXOXTECH 牛牛技术客栈 设计模式-享元模式 - OXOXTECH 牛牛技术客栈 牛牛成长记录 - OXOXTECH 牛牛技术客栈 ffmpeg常用命令 - OXOXTECH 牛牛技术客栈 设计模式-外观模式 - OXOXTECH 牛牛技术客栈 设计模式-装饰器模式 - OXOXTECH 牛牛技术客栈 设计模式-桥接模式 - OXOXTECH 牛牛技术客栈 5周年恋爱纪念日 - OXOXTECH 牛牛技术客栈 2024新年快乐,龙腾四海 - OXOXTECH 牛牛技术客栈 迎接新年:除夕的美好时刻 - OXOXTECH 牛牛技术客栈 设计模式-适配器模式 - OXOXTECH 牛牛技术客栈 设计模式-原型模式 - OXOXTECH 牛牛技术客栈 设计模式-建造者模式 - OXOXTECH 牛牛技术客栈 设计模式-工厂模式 - OXOXTECH 牛牛技术客栈 设计模式-单例模式 - OXOXTECH 牛牛技术客栈 SpringBoot在Linux环境下发送163邮件失败(No appropriate protocol (protocol is disabled or cipher suites are inappropriate)) 海与日落 - OXOXTECH 牛牛技术客栈 Swagger比较常用的注解 - OXOXTECH 牛牛技术客栈 猫🐱牛 - OXOXTECH 牛牛技术客栈 2023年最后一个晚霞 - OXOXTECH 牛牛技术客栈 Linux(Centos)部署Nginx教程 - OXOXTECH 牛牛技术客栈 Linux MySQL下载安装详细教程(CentOS版) - OXOXTECH 牛牛技术客栈 JavaFx打包成exe - OXOXTECH 牛牛技术客栈 Flux脚本语言入门教程 - OXOXTECH 牛牛技术客栈 演唱会出图 - OXOXTECH 牛牛技术客栈 Netty TCP解决粘包拆包 - OXOXTECH 牛牛技术客栈 SpringBoot实现订单超时取消的几种方案 - OXOXTECH 牛牛技术客栈 详解Java并发中的各种锁 - OXOXTECH 牛牛技术客栈 SpringBoot集成支付宝支付 - OXOXTECH 牛牛技术客栈 雪花算法:分布式系统唯一ID生成算法 - OXOXTECH 牛牛技术客栈 Java解决空指针的神器Optional - OXOXTECH 牛牛技术客栈 与兴一起 - OXOXTECH 牛牛技术客栈 Java17新特性详解与安装 - OXOXTECH 牛牛技术客栈 Jdk17安装+环境配置详细教程 - OXOXTECH 牛牛技术客栈 孤注一掷 - OXOXTECH 牛牛技术客栈 解决WinSCP经常断线重连 - OXOXTECH 牛牛技术客栈 内存不足导致Tomcat崩溃问题排查与解决办法 - OXOXTECH 牛牛技术客栈 influxDB初识,一个高效的时序数据库 - OXOXTECH 牛牛技术客栈 SpringBoot 服务接口限流方案 - OXOXTECH 牛牛技术客栈 Docker 安装 Portainer - OXOXTECH 牛牛技术客栈 Linux 安装Docker - OXOXTECH 牛牛技术客栈 物料宣传 - OXOXTECH 牛牛技术客栈 Java使用EMQX实现MQTT通信 - OXOXTECH 牛牛技术客栈 Java实现常见的排序算法 - OXOXTECH 牛牛技术客栈 FreeSwitch Windows安装教程 - OXOXTECH 牛牛技术客栈 MQTT单向SSL数据加密 - OXOXTECH 牛牛技术客栈 随性 - OXOXTECH 牛牛技术客栈 mysql报错Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggre的解决方案 Git Push项目报 push to origin/master was rejected 错误解决方案 游行记——珠海金沙滩与金湖公园之行 - OXOXTECH 牛牛技术客栈 Tomcat:解决Tomcat启动警告:"无法将资源添加到Web应用程序缓存中....请考虑增加缓存空间" 的问题 - OXOXTECH 牛牛技术客栈 励骏庞都广场,迷一般的皇宫 - OXOXTECH 牛牛技术客栈 Docker 常用命令集合 - OXOXTECH 牛牛技术客栈 ElasticSearch Windows版-安装教程 - OXOXTECH 牛牛技术客栈 Java去除对象中为null的字段 - OXOXTECH 牛牛技术客栈 我和我的青春 - OXOXTECH 牛牛技术客栈 Java实现螺旋矩阵算法: - OXOXTECH 牛牛技术客栈 Java直接内存分配和释放的理解 - OXOXTECH 牛牛技术客栈 FreeSwitch将默认数据库迁移至MySQL - OXOXTECH 牛牛技术客栈 别错过路上的风景,别错过刹那间的深情! - OXOXTECH 牛牛技术客栈 Viewer.js:一款强大的图片预览组件 - OXOXTECH 牛牛技术客栈 Java JDK Proxy和CGLib动态代理示例 redis常用命令 - OXOXTECH 牛牛技术客栈 SpringBoot查询IP归属地 - OXOXTECH 牛牛技术客栈 Spring 事务失效的六种情况 #张艺兴每时每刻# - OXOXTECH 牛牛技术客栈
Java 8 Stream计算原理 - OXOXTECH 牛牛技术客栈
Mr.Potato · 2023-02-18 · via

Java 8 Stream简介

从Java 8 开始,我们可以使用Stream接口以及lambda表达式进行“流式计算”。它可以让我们对集合的操作更加简洁、更加可读、更加高效。

Stream接口有非常多用于集合计算的方法,比如判空操作empty、过滤操作filter、求最max值、查找操作findFirst和findAny等等。

Stream单线程串行计算

Stream接口默认是使用串行的方式,也就是说在一个线程里执行。下面举一个例子:

public class StreamDemo {
    public static void main(String[] args) {
        Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
                .reduce((a, b) -> {
                    System.out.println(String.format("%s: %d + %d = %d",
                            Thread.currentThread().getName(), a, b, a + b));
                    return a + b;
                })
                .ifPresent(System.out::println);
    }
}

我们来理解一下这个方法。首先我们用整数1~9创建了一个Stream。这里的Stream.of(T… values)方法是Stream接口的一个静态方法,其底层调用的是Arrays.stream(T[] array)方法。

然后我们使用了reduce方法来计算这个集合的累加和。reduce方法这里做的是:从前两个元素开始,进行某种操作(我这里进行的是加法操作)后,返回一个结果,然后再拿这个结果跟第三个元素执行同样的操作,以此类推,直到最后的一个元素。

我们来打印一下当前这个reduce操作的线程以及它们被操作的元素和返回的结果以及最后所有reduce方法的结果,也就代表的是数字1到9的累加和。

main: 1 + 2 = 3
main: 3 + 3 = 6
main: 6 + 4 = 10
main: 10 + 5 = 15
main: 15 + 6 = 21
main: 21 + 7 = 28
main: 28 + 8 = 36
main: 36 + 9 = 45
45

可以看到,默认情况下,它是在一个单线程运行的,也就是main线程。然后每次reduce操作都是串行起来的,首先计算前两个数字的和,然后再往后依次计算。

Stream多线程并行计算

我们思考上面一个例子,是不是一定要在单线程里进行串行地计算呢?假如我的计算机是一个多核计算机,我们在理论上能否利用多核来进行并行计算,提高计算效率呢?

当然可以,比如我们在计算前两个元素1 + 2 = 3的时候,其实我们也可以同时在另一个核计算 3 + 4 = 7。然后等它们都计算完成之后,再计算 3 + 7 = 10的操作。

是不是很熟悉这样的操作手法?没错,它就是ForkJoin框架的思想。下面小小地修改一下上面的代码,增加一行代码,使Stream使用多线程来并行计算:

public class StreamParallelDemo {
    public static void main(String[] args) {
        Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
                .parallel()
                .reduce((a, b) -> {
                    System.out.println(String.format("%s: %d + %d = %d",
                            Thread.currentThread().getName(), a, b, a + b));
                    return a + b;
                })
                .ifPresent(System.out::println);
    }
}

可以看到,与上一个案例的代码只有一点点区别,就是在reduce方法被调用之前,调用了parallel()方法。下面来看看这个方法的输出:

ForkJoinPool.commonPool-worker-1: 3 + 4 = 7
ForkJoinPool.commonPool-worker-4: 8 + 9 = 17
ForkJoinPool.commonPool-worker-2: 5 + 6 = 11
ForkJoinPool.commonPool-worker-3: 1 + 2 = 3
ForkJoinPool.commonPool-worker-4: 7 + 17 = 24
ForkJoinPool.commonPool-worker-4: 11 + 24 = 35
ForkJoinPool.commonPool-worker-3: 3 + 7 = 10
ForkJoinPool.commonPool-worker-3: 10 + 35 = 45
45

可以很明显地看到,它使用的线程是ForkJoinPool里面的commonPool里面的worker线程。并且它们是并行计算的,并不是串行计算的。但由于Fork/Join框架的作用,它最终能很好的协调计算结果,使得计算结果完全正确。

如果我们用Fork/Join代码去实现这样一个功能,那无疑是非常复杂的。但Java8提供了并行式的流式计算,大大简化了我们的代码量,使得我们只需要写很少很简单的代码就可以利用计算机底层的多核资源。

从源码看Stream并行计算原理

上面我们通过在控制台输出线程的名字,看到了Stream的并行计算底层其实是使用的Fork/Join框架。那它到底是在哪使用Fork/Join的呢?我们从源码上来解析一下上述案例。

Stream.of方法就不说了,它只是生成一个简单的Stream。先来看看parallel()方法的源码。这里由于我的数据是int类型的,所以它其实是使用的BaseStream接口的parallel()方法。而BaseStream接口的JDK唯一实现类是一个叫AbstractPipeline的类。下面我们来看看这个类的parallel()方法的代码:

public final S parallel() {
    sourceStage.parallel = true;
    return (S) this;
}

这个方法很简单,就是把一个标识sourceStage.parallel设置为true。然后返回实例本身。

接着我们再来看reduce这个方法的内部实现。

Stream.reduce()方法的具体实现是交给了ReferencePipeline这个抽象类,它是继承了AbstractPipeline这个类的:

// ReferencePipeline抽象类的reduce方法
@Override
public final Optional<P_OUT> reduce(BinaryOperator<P_OUT> accumulator) {
    // 调用evaluate方法
    return evaluate(ReduceOps.makeRef(accumulator));
}

final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp) {
    assert getOutputShape() == terminalOp.inputShape();
    if (linkedOrConsumed)
        throw new IllegalStateException(MSG_STREAM_LINKED);
    linkedOrConsumed = true;

    return isParallel() // 调用isParallel()判断是否使用并行模式
        ? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags()))
        : terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));
}

@Override
public final boolean isParallel() {
    // 根据之前在parallel()方法设置的那个flag来判断。
    return sourceStage.parallel;
}

从它的源码可以知道,reduce方法调用了evaluate方法,而evaluate方法会先去检查当前的flag,是否使用并行模式,如果是则会调用evaluateParallel方法执行并行计算,否则,会调用evaluateSequential方法执行串行计算。

这里我们再看看TerminalOp(注意这里是字母l O,而不是数字1 0)接口的evaluateParallel方法。TerminalOp接口的实现类有这样几个内部类:

  • java.util.stream.FindOps.FindOp
  • java.util.stream.ForEachOps.ForEachOp
  • java.util.stream.MatchOps.MatchOp
  • java.util.stream.ReduceOps.ReduceOp

可以看到,对应的是Stream的几种主要的计算操作。我们这里的示例代码使用的是reduce计算,那我们就看看ReduceOp类的这个方法的源码:

// java.util.stream.ReduceOps.ReduceOp.evaluateParallel
@Override
public <P_IN> R evaluateParallel(PipelineHelper<T> helper,
                                 Spliterator<P_IN> spliterator) {
    return new ReduceTask<>(this, helper, spliterator).invoke().get();
}

evaluateParallel方法创建了一个新的ReduceTask实例,并且调用了invoke()方法后再调用get()方法,然后返回这个结果。那这个ReduceTask是什么呢?它的invoke方法内部又是什么呢?

追溯源码我们可以发现,ReduceTask类是ReduceOps类的一个内部类,它继承了AbstractTask类,而AbstractTask类又继承了CountedCompleter类,而CountedCompleter类又继承了ForkJoinTask类!

它们的继承关系如下:

ReduceTask -> AbstractTask -> CountedCompleter -> ForkJoinTask

这里的ReduceTask的invoke方法,其实是调用的ForkJoinTaskinvoke方法,中间三层继承并没有覆盖这个方法的实现。

所以这就从源码层面解释了Stream并行的底层原理是使用了Fork/Join框架。

需要注意的是,一个Java进程的Stream并行计算任务默认共享同一个线程池,如果随意的使用并行特性可能会导致方法的吞吐量下降。我们可以通过下面这种方式来让你的某个并行Stream使用自定义的ForkJoin线程池:

ForkJoinPool customThreadPool = new ForkJoinPool(4);
long actualTotal = customThreadPool
  .submit(() -> roster.parallelStream().reduce(0, Integer::sum)).get();

Stream并行计算的性能提升

我们可以在本地测试一下如果在多核情况下,Stream并行计算会给我们的程序带来多大的效率上的提升。用以下示例代码来计算1千万0-100的随机数的和:

 System.out.printf("本计算机的核数:%d%n",
                Runtime.getRuntime().availableProcessors());
        // 产生100w个随机数(1 ~ 100),组成列表
        Random random = new Random();
        List<Integer> list = new ArrayList<>(1000_0000);

        for (int i = 0; i < 1000_00000; i++) {
            list.add(random.nextInt(10));
        }
        long prevTime = getCurrentTime();
        list.stream().reduce(Integer::sum).ifPresent(System.out::println);
        System.out.printf("单线程计算耗时:%d%n",
                getCurrentTime() - prevTime);

        prevTime = getCurrentTime();
        list.stream().parallel().reduce(Integer::sum)
                .ifPresent(System.out::println);
        System.out.printf("多线程计算耗时:%d%n",
                getCurrentTime() - prevTime);
    }
    private static long getCurrentTime() {
        return System.currentTimeMillis();
    }

输出结果:

52446f08a6dd0ef78599f5d042a051ca.png

所以在多核的情况下,使用Stream的并行计算确实比串行计算能带来很大效率上的提升,并且也能保证结果计算完全准确。

备注:如果你的服务器并不是多核服务器,那也没必要用Stream的并行计算。因为在单核的情况下,往往Stream的串行计算比并行计算更快,因为它不需要线程切换的开销。


参考资料:Java 8 Stream并行计算原理