天天看點

【小家java】Stream流操作的有狀态 vs 無狀态

相關閱讀

【小家java】java5新特性(簡述十大新特性) 重要一躍

【小家java】java6新特性(簡述十大新特性) 雞肋更新

【小家java】java7新特性(簡述八大新特性) 不溫不火

【小家java】java8新特性(簡述十大新特性) 飽受贊譽

【小家java】java9新特性(簡述十大新特性) 褒貶不一

【小家java】java10新特性(簡述十大新特性) 小步疊代

【小家java】java11新特性(簡述八大新特性) 首個重磅LTS版本

每篇一句

迷茫,就是不想做小事,隻想做大事。不想做眼前的事,隻想做天邊的事。不想做繁瑣的事,隻想做繁榮的事。其實沒有迷茫,隻有對自己的高估。低下頭,腳踏實地了,這病也就好了

概念解釋

說這個命題之前,我先解釋一下程式設計裡,有狀态和無狀态都什麼意思

  • 有狀态
    • 有狀态就是有資料存儲功能,線程不安全
  • 無狀态
    • 無狀态就是一次操作,不能儲存資料。線程安全

下面我們先看看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差不多,他們接收一個流,再生成一個流,但是差別在于排序和去重複項需要知道先前的曆史。比如排序就需要将所有元素放入緩存區後才能給輸出流加入一個項目,這個操作對緩存的要求是無上限的,流有多大就需要多大的緩存才能進行運算。這些操作也是有狀态操作。

【小家java】Stream流操作的有狀态 vs 無狀态
是以判斷流操作是否有狀态的判斷标準,就是看是否需要知道先前的資料曆史。前後資料是否有依賴關系來判斷

中間操作就像是水管的一部分,終端操作就像水龍頭,增加水管長度不會消耗水,隻有打開水龍頭才會開始消耗水。

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