CodeGym /Các khóa học /JAVA 25 SELF /Spliterator và stream song song

Spliterator và stream song song

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Làm quen với Spliterator

Nếu bạn nghĩ rằng các collection trong Java chỉ được duyệt bằng Iterator, thì trước Java 8 bạn hoàn toàn đúng. Nhưng với sự xuất hiện của Stream API và xu hướng xử lý song song, một nhân vật mới đã xuất hiện — Spliterator.

Spliterator là một interface cho phép không chỉ duyệt các phần tử của collection mà còn chia tách nguồn dữ liệu thành các phần để xử lý song song. Tên gọi là sự kết hợp của các từ splititerator.

Hãy tưởng tượng một chiếc bánh lớn. Iterator bình thường cắt từng miếng và ăn theo thứ tự. Spliterator có thể cắt chiếc bánh làm đôi, đưa một nửa cho bạn — và cả hai bắt đầu ăn đồng thời. Có nhiều bạn — tiếp tục chia tiếp!

Giao diện Spliterator — các phương thức chính

public interface Spliterator<T> {
    boolean tryAdvance(java.util.function.Consumer<? super T> action);
    Spliterator<T> trySplit();
    long estimateSize();
    int characteristics();
    // ... còn vài phương thức nữa, nhưng đây là những phương thức quan trọng nhất
}
  • tryAdvance — thực hiện hành động với phần tử kế tiếp (tương tự next() + hành động).
  • trySplit — cố gắng chia nguồn thành hai phần và trả về một Spliterator mới cho phần “tách ra”.
  • estimateSize — ước lượng còn bao nhiêu phần tử.
  • characteristics — trả về bitmask đặc tính (thứ tự, tính duy nhất, bất biến, v.v.).

2. Sử dụng Spliterator: duyệt thủ công và chia tách

Lấy Spliterator từ collection

Bất kỳ collection nào hiện thực Collection đều có thể cung cấp Spliterator của nó:

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

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

Duyệt phần tử thủ công

Spliterator<String> spliterator = names.spliterator();
while (spliterator.tryAdvance(name -> System.out.println("Tên: " + name))) {
    // Mọi việc diễn ra bên trong tryAdvance
}

Chia tách collection

Điều thú vị nhất là phương thức trySplit():

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

System.out.println("Phần thứ nhất:");
spliterator1.forEachRemaining(System.out::println);

System.out.println("Phần thứ hai:");
if (spliterator2 != null) {
    spliterator2.forEachRemaining(System.out::println);
}

Điều sẽ xảy ra: Spliterator sẽ cố gắng chia collection thành hai phần (không phải lúc nào cũng đúng một nửa — phụ thuộc vào hiện thực). Bây giờ bạn có thể xử lý cả hai phần độc lập — thậm chí ở các luồng khác nhau!

3. Stream song song: để làm gì và hoạt động thế nào

Stream song song (parallelStream()) là stream xử lý các phần tử không theo tuần tự, mà đồng thời ở nhiều luồng. Đặc biệt hữu ích với khối lượng dữ liệu lớn và bộ xử lý đa nhân.

import java.util.List;

List<String> names = List.of("Vasya", "Petya", "Masha", "Lena");
// Stream thường:
names.stream().forEach(System.out::println);
// Stream song song:
names.parallelStream().forEach(System.out::println);

Điểm mấu chốt là gì?
Ở stream thường, các phần tử được xử lý trong một luồng. Ở stream song song — nguồn được chia thành các phần (bằng Spliterator), và mỗi phần được xử lý ở một luồng riêng.

Bên trong hoạt động ra sao?

  1. Spliterator chia collection thành các phần — thường theo số lõi có sẵn (hoặc nhiều hơn một chút).
  2. Mỗi phần được xử lý trong luồng riêng — dùng ForkJoinPool chung.
  3. Kết quả được thu thập lại — hợp nhất thành collection hoặc giá trị cuối.

Sơ đồ hoạt động của stream song song

flowchart LR
    A[Bộ sưu tập] --> B{Spliterator}
    B --> C1[Phần 1] --> D1[Luồng 1]
    B --> C2[Phần 2] --> D2[Luồng 2]
    B --> C3[Phần 3] --> D3[Luồng 3]
    D1 & D2 & D3 --> E[Tổng hợp kết quả]

4. Ưu điểm và hạn chế của stream song song

Ưu điểm

  • Tăng tốc xử lý các collection lớn: với tính toán nặng, stream song song giúp tăng tốc đáng kể.
  • Đơn giản: không cần tự viết code đa luồng — chỉ việc thay stream() bằng parallelStream().

Hạn chế và cạm bẫy

  • Không phải lúc nào cũng nhanh hơn: với collection nhỏ, chi phí chia tách và lập lịch có thể “ăn hết” lợi ích.
  • Không đảm bảo thứ tự: trong forEach/map/filter thứ tự có thể khác. Nếu cần thứ tự — hãy dùng forEachOrdered.
  • Vấn đề về an toàn luồng (thread safety): các thao tác có hiệu ứng phụ (thay đổi collection/biến bên ngoài) dẫn tới race condition.
  • Không phải mọi thao tác đều phù hợp: các tính toán phụ thuộc (ví dụ, cộng dồn tuần tự) có thể không hoạt động như mong muốn.

Khi nào nên dùng stream song song?

  • Collection lớn (hàng chục nghìn phần tử trở lên).
  • Tác vụ nặng trên mỗi phần tử.
  • Không yêu cầu thứ tự nghiêm ngặt.
  • Không có hiệu ứng phụ (hàm thuần).

Khi nào KHÔNG nên dùng?

  • Ít phần tử.
  • Code thay đổi biến hoặc collection bên ngoài.
  • Cần giữ nguyên thứ tự xử lý.
  • Nguồn dữ liệu khó chia (ví dụ, LinkedList).

5. Ví dụ thực tiễn

Ví dụ 1: So sánh thời gian thực thi

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("Stream thường: " + time + " ms, số nguyên tố tìm được: " + count);

        start = System.currentTimeMillis();
        count = numbers.parallelStream()
                .filter(n -> isPrime(n))
                .count();
        time = System.currentTimeMillis() - start;
        System.out.println("Stream song song: " + time + " ms, số nguyên tố tìm được: " + count);
    }

    // Kiểm tra số nguyên tố đơn giản (cho ví dụ)
    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;
    }
}

Kết quả: với khối lượng dữ liệu lớn, stream song song thường nhanh hơn (đặc biệt trên CPU đa nhân). Với dữ liệu nhỏ — có thể không khác biệt hoặc phương án song song còn chậm hơn.

Ví dụ 2: Vấn đề về thứ tự

import java.util.List;

List<String> names = List.of("Vasya", "Petya", "Masha", "Lena");
System.out.println("Stream thường:");
names.stream().forEach(System.out::println);

System.out.println("Stream song song:");
names.parallelStream().forEach(System.out::println);

System.out.println("Stream song song với forEachOrdered:");
names.parallelStream().forEachOrdered(System.out::println);

Kết luận: ở stream thường và khi dùng forEachOrdered thì thứ tự được giữ, còn ở stream song song không dùng nó — thì không.

Ví dụ 3: Nguy hiểm của hiệu ứng phụ

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

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

// NGUY HIỂM! Đừng làm vậy!
numbers.parallelStream().forEach(n -> results.add(n * n));

System.out.println("Kích thước danh sách: " + results.size());

Điều gì có thể xảy ra? Kích thước danh sách có thể nhỏ hơn mong đợi, và đôi khi sẽ xuất hiện ConcurrentModificationException. Nguyên nhân — ArrayList không an toàn luồng, còn stream song song chạy nhiều luồng đồng thời.

6. Spliterator: đặc điểm và đặc tính

Các đặc tính của Spliterator

Spliterator mô tả thuộc tính của mình qua một bitmask:

  • ORDERED — các phần tử có thứ tự xác định (ví dụ: list).
  • DISTINCT — tất cả phần tử là duy nhất (ví dụ: set).
  • SORTED — các phần tử đã được sắp xếp.
  • SIZED — biết kích thước.
  • IMMUTABLE — collection bất biến.
  • CONCURRENT — collection an toàn luồng.
  • SUBSIZED — tất cả các spliterator sau trySplit() cũng biết kích thước của mình.
Spliterator<String> spliterator = names.spliterator();
int characteristics = spliterator.characteristics();
System.out.println(Integer.toBinaryString(characteristics));

Tại sao cần biết điều này? Stream API và stream song song dùng các đặc tính này để tối ưu. Ví dụ, nếu nguồn bất biến và đã sắp xếp, có thể chia và thu thập kết quả an toàn và hiệu quả hơn.

7. Khi nào và cách dùng Spliterator trực tiếp?

Trong thực tế hằng ngày hiếm khi phải tự viết Spliterator: các collection chuẩn đã hiện thực sẵn. Nhưng nếu bạn tạo nguồn dữ liệu riêng hoặc muốn kiểm soát chi tiết việc duyệt/chia tách, Spliterator sẽ hữu ích.

Ví dụ: duyệt thủ công với 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("Phần tử đầu tiên: " + name));
spliterator.forEachRemaining(name -> System.out.println("Còn lại: " + name));

Ví dụ: chia tách collection

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

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

8. Lỗi thường gặp khi làm việc với Spliterator và stream song song

Lỗi №1: Dùng stream song song cho collection nhỏ. Thay vì tăng tốc, bạn sẽ bị chậm — chi phí chia tách và lập lịch tác vụ vượt quá lợi ích.

Lỗi №2: Kỳ vọng giữ nguyên thứ tự phần tử. Stream song song không đảm bảo thứ tự. Nếu quan trọng — hãy dùng forEachOrdered, nhưng hiệu quả song song sẽ giảm.

Lỗi №3: Hiệu ứng phụ trong biểu thức lambda. Bên trong stream song song không thể an toàn khi thay đổi biến/collection bên ngoài — sẽ gặp race condition và bug khó lần ra.

Lỗi №4: Dùng collection không an toàn bên trong stream song song. Thêm phần tử vào ArrayList thông thường từ nhiều luồng — con đường dẫn tới lỗi như ConcurrentModificationException.

Lỗi №5: Kỳ vọng tăng tốc tức thì. Stream song song không phải đũa thần. Hãy đo đạc/profiling: nếu dữ liệu ít hoặc thao tác nhẹ — stream thường nhanh hơn.

Lỗi №6: Stream song song với nguồn khó chia. Ví dụ, LinkedList thường chia kém hiệu quả — độ song song có thể chỉ làm chậm thêm.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION