CodeGym /課程 /JAVA 25 SELF /Spliterator 與平行串流

Spliterator 與平行串流

JAVA 25 SELF
等級 33 , 課堂 3
開放

1. 認識 Spliterator

如果你以為 Java 中的集合只能透過 Iterator 來遍歷,那在 Java 8 之前你完全正確。但隨著 Stream API 與平行化風潮的到來,出現了新主角 — Spliterator

Spliterator 是一個介面,它不僅能遍歷集合中的元素,還能切分資料來源,使其可被平行處理。名稱是 splititerator 的結合。

想像一個大蛋糕。一般的 Iterator 會一塊一塊按順序吃。Spliterator 可以把蛋糕一分為二,分一半給朋友 — 於是你們可以同時開吃。朋友很多?那就繼續切!

Spliterator 介面:主要方法

public interface Spliterator<T> {
    boolean tryAdvance(java.util.function.Consumer<? super T> action);
    Spliterator<T> trySplit();
    long estimateSize();
    int characteristics();
    // ... 還有幾個方法,但這些是最重要的
}
  • tryAdvance — 對下一個元素執行動作(類似 next() + 動作)。
  • trySplit — 嘗試把來源分成兩部分,並為「分出」的那一部分回傳新的 Spliterator
  • estimateSize — 估計剩餘元素數量。
  • characteristics — 回傳特性位元遮罩(是否有序、是否唯一、是否不可變等)。

2. 使用 Spliterator:手動遍歷與切分

從集合取得 Spliterator

任何實作了 Collection 的集合都能提供自己的 Spliterator

import java.util.List;
import java.util.Spliterator;

List<String> names = List.of("Vasya", "Petya", "Masha", "Lena");
Spliterator<String> spliterator = names.spliterator();

手動遍歷元素

Spliterator<String> spliterator = names.spliterator();
while (spliterator.tryAdvance(name -> System.out.println("姓名:" + name))) {
    // 一切都在 tryAdvance 內完成
}

切分集合

最有趣的是方法 trySplit()

Spliterator<String> spliterator1 = names.spliterator();
Spliterator<String> spliterator2 = spliterator1.trySplit();

System.out.println("第一部分:");
spliterator1.forEachRemaining(System.out::println);

System.out.println("第二部分:");
if (spliterator2 != null) {
    spliterator2.forEachRemaining(System.out::println);
}

會發生什麼:Spliterator 會嘗試將集合分成兩部分(不一定剛好一半 — 取決於實作)。現在你可以獨立處理這兩部分 — 甚至在不同的執行緒中!

3. 平行串流:為什麼以及如何運作

平行串流(parallelStream())會同時在多個執行緒中處理元素。當資料量大且處理器為多核心時特別有用。

import java.util.List;

List<String> names = List.of("Vasya", "Petya", "Masha", "Lena");
// 一般串流:
names.stream().forEach(System.out::println);
// 平行串流:
names.parallelStream().forEach(System.out::println);

重點是什麼?
在一般串流中,元素在單一執行緒中處理;而在平行串流中,來源會(透過 Spliterator)被切分成多部分,每一部分在不同的執行緒中處理。

內部如何運作?

  1. Spliterator 會把集合切分成多部分 — 通常依可用核心數(或稍多)。
  2. 每一部分在自己的執行緒中處理 — 使用共用的 ForkJoinPool
  3. 最後將結果彙整回來 — 合併為最終的集合或值。

平行串流的工作流程示意

flowchart LR
    A[集合] --> B{Spliterator}
    B --> C1[部分 1] --> D1[執行緒 1]
    B --> C2[部分 2] --> D2[執行緒 2]
    B --> C3[部分 3] --> D3[執行緒 3]
    D1 & D2 & D3 --> E[結果彙整]

4. 平行串流的優勢與限制

優點

  • 加速大量集合的處理:在計算密集的情況下,平行串流能明顯縮短時間。
  • 簡單:不必手寫多執行緒程式碼 — 把 stream() 換成 parallelStream() 即可。

限制與地雷

  • 不一定更快:對小型集合,開銷可能「吃掉」效益。
  • 不保證順序:在 forEach/map/filter 中順序可能不同。若需要順序,使用 forEachOrdered
  • 執行緒安全問題:具有副作用的操作(修改外部集合/變數)會導致資料競爭。
  • 不是所有操作都適合:相依的計算(例如序列性累加)可能與預期不符。

何時該使用平行串流?

  • 大量集合(數萬筆以上)。
  • 每個元素需要耗時/繁重運算。
  • 不嚴格要求順序。
  • 沒有副作用(純函式)。

何時不要使用?

  • 元素很少。
  • 程式碼會修改外部變數或集合。
  • 必須保留處理順序。
  • 資料來源不易切分(例如 LinkedList)。

5. 實用範例

範例 1:執行時間比較

import java.util.*;
import java.util.stream.*;

public class ParallelStreamDemo {
    public static void main(String[] args) {
        List<Integer> numbers = IntStream.range(0, 10_000_000)
                                         .boxed()
                                         .collect(Collectors.toList());

        long start = System.currentTimeMillis();
        long count = numbers.stream()
                .filter(n -> isPrime(n))
                .count();
        long time = System.currentTimeMillis() - start;
        System.out.println("一般串流:" + time + " 毫秒,找到的質數:" + count);

        start = System.currentTimeMillis();
        count = numbers.parallelStream()
                .filter(n -> isPrime(n))
                .count();
        time = System.currentTimeMillis() - start;
        System.out.println("平行串流:" + time + " 毫秒,找到的質數:" + count);
    }

    // 最簡單的質數檢查(示例用)
    public static boolean isPrime(int n) {
        if (n < 2) return false;
        for (int i = 2, sqrt = (int)Math.sqrt(n); i <= sqrt; i++)
            if (n % i == 0) return false;
        return true;
    }
}

結果:在大量資料上,平行串流常常更快(尤其在多核心處理器上)。在小資料集上,可能沒有差異,甚至平行版本會更慢。

範例 2:順序問題

import java.util.List;

List<String> names = List.of("Vasya", "Petya", "Masha", "Lena");
System.out.println("一般串流:");
names.stream().forEach(System.out::println);

System.out.println("平行串流:");
names.parallelStream().forEach(System.out::println);

System.out.println("使用 forEachOrdered 的平行串流:");
names.parallelStream().forEachOrdered(System.out::println);

結論:在一般串流以及使用 forEachOrdered 時能保持順序;未使用它的平行串流則無法保證順序。

範例 3:副作用的危險

import java.util.*;
import java.util.stream.*;

List<Integer> numbers = IntStream.range(1, 1000).boxed().collect(Collectors.toList());
List<Integer> results = new ArrayList<>();

// 危險!不要這麼做!
numbers.parallelStream().forEach(n -> results.add(n * n));

System.out.println("清單大小:" + results.size());

可能發生什麼?清單大小可能小於預期,有時還會出現 ConcurrentModificationException。原因是 ArrayList 非執行緒安全,而平行串流會同時啟動多個執行緒。

6. Spliterator:特性與要點

Spliterator 的特性

Spliterator 以位元遮罩描述其屬性:

  • ORDERED — 元素具有固定順序(例如 List)。
  • DISTINCT — 所有元素唯一(例如 Set)。
  • SORTED — 元素已排序。
  • SIZED — 已知大小。
  • IMMUTABLE — 集合不可變。
  • CONCURRENT — 集合具執行緒安全。
  • SUBSIZED — 經過 trySplit() 之後的各個 spliterator 也知道自身大小。
Spliterator<String> spliterator = names.spliterator();
int characteristics = spliterator.characteristics();
System.out.println(Integer.toBinaryString(characteristics));

為什麼要知道這些?Stream API 與平行串流會利用這些特性進行最佳化。比方說,若來源不可變且已排序,就能更安全且更有效率地切分與彙整結果。

7. 何時以及如何直接使用 Spliterator?

日常開發中很少需要自己撰寫 Spliterator:標準集合已經實作好了。不過,如果你建立自訂的資料來源,或想細緻控制遍歷/切分,Spliterator 就很有用。

範例:使用 tryAdvance 的手動遍歷

import java.util.List;
import java.util.Spliterator;

List<String> names = List.of("Vasya", "Petya", "Masha", "Lena");
Spliterator<String> spliterator = names.spliterator();
spliterator.tryAdvance(name -> System.out.println("第一個元素:" + name));
spliterator.forEachRemaining(name -> System.out.println("其餘:" + name));

範例:切分集合

Spliterator<String> spliterator1 = names.spliterator();
Spliterator<String> spliterator2 = spliterator1.trySplit();

if (spliterator2 != null) {
    spliterator2.forEachRemaining(name -> System.out.println("第 2 部分:" + name));
}
spliterator1.forEachRemaining(name -> System.out.println("第 1 部分:" + name));

8. 使用 Spliterator 與平行串流的常見錯誤

錯誤 #1:對小型集合使用平行串流。 加速不成反而變慢 — 切分與排程的額外成本會超過收益。

錯誤 #2:期待保留元素順序。 平行串流不保證順序。若順序很重要 — 請使用 forEachOrdered,但一部分平行效能會流失。

錯誤 #3:在 lambda 中產生副作用。 在平行串流內無法安全地修改外部變數/集合 — 會導致資料競爭與難以察覺的 bug。

錯誤 #4:在平行串流中使用非執行緒安全的集合。 從多執行緒向一般的 ArrayList 新增元素 — 容易引發 ConcurrentModificationException 等錯誤。

錯誤 #5:期待立刻加速。 平行串流不是萬靈丹。請先進行效能剖析:若資料量小或操作很輕量 — 一般串流反而更快。

錯誤 #6:對不易切分的來源使用平行串流。 例如,LinkedList 往往切分效率不佳 — 平行化可能只會拖慢速度。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION