【小家java】Stream流操作的有状态 vs 无状态

概念解释

说这个命题之前,我先解释一下编程里,有状态和无状态都什么意思

  • 有状态
    • 有状态就是有数据存储功能,线程不安全
  • 无状态
    • 无状态就是一次操作,不能保存数据。线程安全

下面我们先看看Srping中的Bean来辅助理解:

Spring中的有状态(Stateful)和无状态(Stateless)

  1. 无状态的Bean适合用不变模式,技术就是单例模式,这样可以共享实例,提高性能。有状态的Bean,多线程环境下不安全,那么适合用Prototype原型模式。Prototype: 每次对bean的请求都会创建一个新的bean实例。
  2. 默认情况下,从Spring bean工厂所取得的实例为singleton(scope属性为singleton),容器只存在一个共享的bean实例
  3. Service层、Dao层用默认singleton就行,虽然Service类也有dao这样的属性,但dao这些类都是没有状态信息的,也就是相当于不变(immutable)类,所以不影响。Struts2中的Action因为会有User、BizEntity这样的实例对象,是有状态信息的,在多线程环境下是不安全的,所以Struts2默认的实现是Prototype模式。在Spring中,Struts2的Action中,scope要配成prototype作用域

Servlet、Out,Request,Response,Session,Config,Page,PageContext是线程安全的,Application在整个系统内被使用,所以不是线程安全的.

Stream流操作的有状态 vs 无状态

比如map或者filter会从输入流中获取每一个元素,并且在输出流中得到一个结果,这些操作没有内部状态,称为无状态操作。 但是像reduce、sum、max这些操作都需要内部状态来累计计算结果,所以称为有状态操作。

这里需要单独解释一下: 有一些操作sort、distinct、limit、skip看上去和filter、map差不多,他们接收一个流,再生成一个流,但是区别在于排序和去重复项需要知道先前的历史。比如排序就需要将所有元素放入缓存区后才能给输出流加入一个项目,这个操作对缓存的要求是无上限的,流有多大就需要多大的缓存才能进行运算。这些操作也是有状态操作。

所以判断流操作是否有状态的判断标准,就是看是否需要知道先前的数据历史。前后数据是否有依赖关系来判断

中间操作就像是水管的一部分,终端操作就像水龙头,增加水管长度不会消耗水,只有打开水龙头才会开始消耗水。

peek方法一般在debug的时候才会开启

下面举个例子,体验一把有状态和无状态:

String str = "my name is fangshixiang";
        Stream.of(str.split(" ")).peek(x -> System.out.println(Thread.currentThread().getName() + "___" + x))
                .map(x -> x.length()).count();

输出:
main___my
main___name
main___is
main___fangshixiang

我们发现,打印的线程都是main主线程,因为我们是串行流嘛。 现在我们试试并行流打印输出:

public static void main(String[] args) {
        //打印每个单词的长度
        String str = "my name is fangshixiang";
        Stream.of(str.split(" ")).parallel().peek(x -> System.out.println(Thread.currentThread().getName() + "___" + x))
                .map(x -> x.length()).count();

    }
输出:
ForkJoinPool.commonPool-worker-1___fangshixiang
main___is
ForkJoinPool.commonPool-worker-2___name
ForkJoinPool.commonPool-worker-3___my

看出效果了没?并行流输出无顺序。并行流采用的ForkJoin框架的线程池ForkJoinPool.commonPool。所有的并行流都会使用同一个线程池,所以如果并行流太多的话,也会出现阻塞的。因此若需要,我们可以让它使用我们自己的线程池,来提高效率。

备注ForkJoinPool.commonPool的线程池大小默认为CPU核心数量。当然可以通脱System.setProperty手动修改这个值,只是一般都没有需要

自定义线程池例子:

 public static void main(String[] args) {
        //定义自己的线程池
        ForkJoinPool pool = new ForkJoinPool(10);

        String str = "my name is fangshixiang";
        pool.execute(() -> Stream.of(str.split(" ")).parallel().peek(x -> System.out.println(Thread.currentThread().getName() + "___" + x))
                .map(x -> x.length()).count());
        pool.shutdown(); //关闭线程池(一般都不需要关的)

        //下面代码是为了让main线程不要退出,因为如果退出太早,上面不会有东西输出的
        //提示一点 wait外面必须有synchronized关键字,因为没有会报错的
        synchronized (pool) {
            try {
                pool.wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
输出:
ForkJoinPool-1-worker-2___name
ForkJoinPool-1-worker-13___my
ForkJoinPool-1-worker-9___is
ForkJoinPool-1-worker-11___fangshixiang

可以明显看出,这里输出的就是我们自己自定义的线程池了,就能很好的保证效率了。

无状态演示:(必须那并行流演示) 因为串行流将没有任何效果,因为是线程安全的

public static void main(String[] args) {
        //打印每个单词的长度
        String str = "my name is fangshixiang good";
        Stream.of(str.split(" "))
                .parallel()
                .peek(x -> System.out.println(Thread.currentThread().getName() + "___" + x))
                .map(x -> x.length())
                //.sorted()
                .peek(x -> System.out.println(Thread.currentThread().getName() + "___" + x))
                .count();

    }
输出:
ForkJoinPool.commonPool-worker-1___name
ForkJoinPool.commonPool-worker-3___good
ForkJoinPool.commonPool-worker-1___4
ForkJoinPool.commonPool-worker-1___fangshixiang
ForkJoinPool.commonPool-worker-1___12
ForkJoinPool.commonPool-worker-2___my
ForkJoinPool.commonPool-worker-2___2
main___is
main___2
ForkJoinPool.commonPool-worker-3___4

虽然每次执行的顺序不一样,但是每次main都是处理is和2。再看下面加入sorted

第一次执行:
main___good
ForkJoinPool.commonPool-worker-1___name
ForkJoinPool.commonPool-worker-1___my
main___fangshixiang
ForkJoinPool.commonPool-worker-1___is
main___4
ForkJoinPool.commonPool-worker-3___2
ForkJoinPool.commonPool-worker-2___4
ForkJoinPool.commonPool-worker-1___12
ForkJoinPool.commonPool-worker-3___2

再次执行:
main___good
ForkJoinPool.commonPool-worker-2___is
main___fangshixiang
ForkJoinPool.commonPool-worker-1___name
ForkJoinPool.commonPool-worker-3___my
main___4
ForkJoinPool.commonPool-worker-1___12
main___4
ForkJoinPool.commonPool-worker-2___2
ForkJoinPool.commonPool-worker-3___2

我们会发现每次的结果都不一样,原因就是因为sorted是有状态的,所以有了很多的不确定性。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏全栈开发之路

JS中数组的深拷贝和浅拷贝

变量保存的是一个指针,存放在栈内存中,指针指向一个位置,这个位置就是存放在堆内存引用类型的值

44420
来自专栏全栈开发之路

一个xss漏洞问题分析

http://zhibo.sogou.com/gameZone_格斗游戏.whtml/gameZone_格斗游戏.whtml?product=live&page...

8030
来自专栏全栈开发之路

Promise.all在统计WebHDFS时的使用

3、对请求到的日志里的数据,分析、截取、分割操作 1)把结果导入数据库(按项目名、日期、项目id、死链、日志原始链接) 2)拼成html邮件格式(table...

7630
来自专栏全栈开发之路

px和rem的换算与设置

一般所有浏览器的html根元素font-size:16px 1rem = 16px 所以所有px值都基于它来换算 设计稿px/换算的rem = 16px ...

48610
来自专栏全栈开发之路

六步搭建ES6语法环境

8340
来自专栏老码农的一亩三分地

IT兄弟连 HTML5教程 HTML5和HTML的关系

HTML5开发现在很火爆,是一门技术,更是一个概念。可以让我们的工作模式、交互模式以及对应用和游戏的体验有了翻天覆地的变化,很多人都知道HTML5这门技术,也常...

10920
来自专栏全栈开发之路

es6将txt数据序列化成json

2、str=(txt里的数据),let arrStr = str.split('\n');

8820
来自专栏全栈开发之路

抛开vue-cli,一步步搭建vue+webpack环境(迁移到webpack4)

抛开vue-cli,一步步搭建vue+webpack环境 https://www.jianshu.com/p/e735e67de774 这是之前写的,当时用...

11820
来自专栏sktj

flask base.html解析(flask 47)

{% from 'bootstrap/nav.html' import render_nav_item %} <!DOCTYPE html> <html l...

7510
来自专栏Java帮帮-微信公众号-技术文章全总结

浏览器大战与 JavaScript 的诞生

在 Android 设备或 iPhone 流行之前,浏览器大战的战场是桌面计算机平台。众多公司投入了数十亿美元投身这场战役,而他们的出发点就是基于这样的一个假设...

4710

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励