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ừ split và iterator.
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?
- 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).
- Mỗi phần được xử lý trong luồng riêng — dùng ForkJoinPool chung.
- 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.
GO TO FULL VERSION