iterator pattern

- Published on
- /11 mins read/
Trong danh mục các mẫu thiết kế hướng đối tượng (GoF), Iterator Pattern là một trong những mẫu phổ biến đến mức hầu hết các kỹ sư phần mềm sử dụng nó hàng ngày mà không nhận ra: từ vòng lặp for-each, java.util.Iterator, đến các luồng dữ liệu vô hạn trong Stream API.
Về mặt lý thuyết, mục đích của Iterator rất thanh lịch: Cung cấp một phương thức truy cập tuần tự vào các phần tử của một tập hợp mà không để lộ cấu trúc dữ liệu bên dưới (dù đó là Array, Doubly Linked List, Red-Black Tree hay B-Tree).
Tuy nhiên, trong các hệ thống doanh nghiệp chịu tải lớn và xử lý đồng thời (Concurrent Systems), Iterator ẩn chứa nhiều "cái bẫy" chí tử:
- Tại sao vòng lặp
for-eachlại ném ra ngoại lệConcurrentModificationExceptionbí ẩn? - Cơ chế
modCounthoạt động như thế nào trong mã nguồn OpenJDK? - Tại sao Iterator cổ điển bất lực trước tính toán đa luồng song song, buộc Java phải phát minh ra
Spliterator? - Làm thế nào để duyệt qua hàng chục triệu bản ghi database mà không gây tràn bộ nhớ Heap (OOM)?
Bài viết này sẽ phân tích toàn diện các vấn đề trên dưới góc nhìn của một Kỹ sư Hệ thống và Kiến trúc sư Phần mềm.
# cơ chế fail-fast và bí ẩn biến modCount trong openjdk
Lỗi kinh điển nhất mà mọi lập trình viên Java từng gặp phải là xóa phần tử trong khi đang duyệt vòng lặp for-each:
List<String> servers = new ArrayList<>(List.of("srv-1", "srv-2", "srv-3"));
for (String server : servers) {
if (server.equals("srv-2")) {
servers.remove(server); // 🔴 NÉM RA ConcurrentModificationException!
}
}# tại sao điều này xảy ra?
Vòng lặp for-each thực chất là cú pháp đường (syntactic sugar) được trình biên dịch dịch thành một vòng lặp Iterator:
Iterator<String> it = servers.iterator();
while (it.hasNext()) {
String server = it.next();
if (server.equals("srv-2")) {
servers.remove(server); // Gọi trực tiếp hàm remove() của List!
}
}Hãy nhìn vào mã nguồn nội bộ của ArrayList.java trong OpenJDK:
# bản chất của modCount
modCount(modification count) là một biến nguyên thủy đếm số lần cấu trúc của collection bị biến đổi (thêm, xóa phần tử, resize mảng).- Khi Iterator được tạo ra, nó chụp một bản snapshot:
expectedModCount = modCount. - Mỗi lần gọi
next(), Iterator thực hiện phép kiểm tra:final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } - Giải pháp an toàn: Luôn dùng
iterator.remove(). Phương thức này xóa phần tử thông qua Iterator và tự động đồng bộ hóa:expectedModCount = modCount, ngăn ngừa xung đột xảy ra.
# fail-fast vs fail-safe / weakly consistent trong concurrency
Trong môi trường đa luồng (Multi-threading), cơ chế Fail-Fast của các Collection truyền thống (ArrayList, HashSet, HashMap) trở thành trở ngại lớn vì một luồng ghi có thể làm sập toàn bộ các luồng đọc.
Java cung cấp hai trường phái tiếp cận khác biệt trong gói java.util.concurrent:
# CopyOnWriteArrayList (fail-safe via snapshot)
- Khi luồng ghi gọi
add()hoặcremove(), nó sao chép toàn bộ mảng dữ liệu sang một vùng nhớ mới. - Iterator duyệt trên mảng cũ tại thời điểm khởi tạo, hoàn toàn miễn nhiễm với thay đổi.
- Trade-off: Cực kỳ tốn RAM nếu tập dữ liệu lớn. Chỉ phù hợp cho cấu hình read-heavy (99% đọc, 1% ghi) như danh sách Listener hoặc Route Table.
# ConcurrentHashMap (weakly consistent)
- Không sao chép toàn bộ bảng băm.
- Iterator duyệt trực tiếp qua các node của bucket table. Nếu một luồng khác thêm node vào sau vị trí con trỏ đang duyệt, Iterator có thể hoặc không phản ánh phần tử mới đó.
- Không bao giờ ném
ConcurrentModificationException, đạt throughput đọc cực lớn.
# cuộc tiến hóa lên Spliterator: mở khóa parallel streams (Java 8+)
Iterator truyền thống của GoF được thiết kế cho kiến trúc máy tính đơn nhân (Single-core CPU). Nó chỉ có hai hàm hasNext() và next() — một quá trình tuần tự tuyệt đối (strictly sequential) với độ phức tạp O(N). Bạn không thể chia một Iterator đơn lẻ cho 16 worker threads cùng xử lý song song mà không tốn công đồng bộ hóa khóa (Lock contention).
Để giải quyết giới hạn này cho Stream API và ForkJoinPool, Java 8 đã giới thiệu Spliterator (Splitable Iterator):
# các phương thức cốt lõi của Spliterator
boolean tryAdvance(Consumer<? super T> action): Tương đương vớinext()của Iterator, xử lý một phần tử duy nhất.Spliterator<T> trySplit(): Trái tim của tính toán song song. Chia tách phân vùng dữ liệu hiện tại làm đôi. Nếu có thể chia được, trả về mộtSpliteratormới đại diện cho nửa đầu để thread khác xử lý, trong khi thread hiện tại tiếp tục với nửa sau.long estimateSize(): Ước lượng số phần tử còn lại đểForkJoinPoolquyết định có nên tiếp tục chia nhỏ hay không.int characteristics(): Trả về mặt nạ bit (Bitmask) mô tả thuộc tính của dữ liệu, giúp Stream Engine tối ưu hóa:ORDERED: Dữ liệu có thứ tự xác định.DISTINCT: Không có phần tử trùng lặp.SORTED: Đã được sắp xếp sẵn.SIZED: Biết chính xác kích thước trước khi duyệt.CONCURRENT: An toàn khi sửa đổi đồng thời.
# xây dựng cursor streaming Iterator: Stream 10 triệu bản ghi không tràn ram
Trong bài toán Enterprise: Bạn cần export báo cáo từ bảng cơ sở dữ liệu có 10,000,000 giao dịch ra file CSV hoặc gửi qua Kafka.
Nếu bạn dùng Spring Data JPA:
List<Transaction> all = transactionRepository.findAll(); // 🔴 SẬP TOÀN BỘ SERVER VỚI OutOfMemoryError!Heap 4GB sẽ cạn kiệt ngay lập tức vì 10 triệu entity ngốn hàng chục gigabyte bộ nhớ.
# giải pháp: triển khai Keyset Pagination Iterator (streaming cursor)
Bằng cách bọc cơ chế Keyset Pagination (Seek Method) bên dưới một Iterator, chúng ta có thể duyệt qua hàng chục triệu bản ghi với dung lượng bộ nhớ RAM cố định ở mức O(1) (chỉ giữ đúng một batch trong bộ nhớ tại bất kỳ thời điểm nào):
# triển khai mã nguồn chuẩn enterprise
package com.company.common.iterator;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.function.Function;
public class KeysetPaginationIterator<T, ID extends Comparable<ID>> implements Iterator<T> {
private final Function<ID, List<T>> pageFetcher;
private final Function<T, ID> idExtractor;
private final int batchSize;
private List<T> currentBatch = Collections.emptyList();
private int cursor = 0;
private ID lastSeenId = null;
private boolean isExhausted = false;
public KeysetPaginationIterator(
Function<ID, List<T>> pageFetcher,
Function<T, ID> idExtractor,
int batchSize) {
this.pageFetcher = pageFetcher;
this.idExtractor = idExtractor;
this.batchSize = batchSize;
}
@Override
public boolean hasNext() {
if (cursor < currentBatch.size()) {
return true;
}
if (isExhausted) {
return false;
}
// Tải mẻ dữ liệu kế tiếp từ DB
fetchNextBatch();
return !currentBatch.isEmpty();
}
@Override
public T next() {
if (!hasNext()) {
throw new NoSuchElementException("Đã duyệt hết dữ liệu!");
}
T item = currentBatch.get(cursor++);
lastSeenId = idExtractor.apply(item);
return item;
}
private void fetchNextBatch() {
currentBatch = pageFetcher.apply(lastSeenId);
cursor = 0;
if (currentBatch.size() < batchSize) {
// Khi số lượng bản ghi trả về nhỏ hơn batchSize -> Đã chạm đến đáy dữ liệu
isExhausted = true;
}
}
}# sử dụng trong pipeline export dữ liệu
@Service
public class TransactionExportService {
@Autowired
private JdbcTemplate jdbcTemplate;
public void exportMillionsOfRecordsToCsv(OutputStream outputStream) throws IOException {
var iterator = new KeysetPaginationIterator<TransactionRecord, Long>(
// Lambda truy vấn DB theo batch với Seek Method (cực nhanh có Index)
lastId -> jdbcTemplate.query("""
SELECT id, account_id, amount, status, created_at
FROM transactions
WHERE (? IS NULL OR id > ?)
ORDER BY id ASC
LIMIT 2000
""",
ps -> {
if (lastId == null) {
ps.setNull(1, java.sql.Types.BIGINT);
ps.setNull(2, java.sql.Types.BIGINT);
} else {
ps.setLong(1, lastId);
ps.setLong(2, lastId);
}
},
new TransactionRowMapper()
),
TransactionRecord::id,
2000
);
try (var writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) {
writer.write("id,account_id,amount,status,created_at\n");
// Xử lý từng phần tử: Bộ nhớ RAM tiêu thụ chỉ ~5MB bất kể dữ liệu 10 triệu hay 1 tỷ dòng!
while (iterator.hasNext()) {
TransactionRecord tx = iterator.next();
writer.write(String.format("%d,%s,%.2f,%s,%s\n",
tx.id(), tx.accountId(), tx.amount(), tx.status(), tx.createdAt()));
}
writer.flush();
}
}
}# ma trận đánh giá so sánh các cơ chế duyệt trong Java
| Tiêu chí | java.util.Iterator | for-each loop | CopyOnWriteArrayList | ConcurrentHashMap | Spliterator |
|---|---|---|---|---|---|
| Cơ chế | Con trỏ đối tượng | Cú pháp bọc ngoài Iterator | Sao chép mảng Snapshot | Duyệt bucket trực tiếp | Chia để trị (Fork-Join) |
| An toàn sửa đổi | Fail-Fast (modCount) | Fail-Fast (modCount) | Fail-Safe | Weakly Consistent | Tùy thuộc data source |
| Chi phí bộ nhớ | 1 Object Iterator nhỏ | 1 Object Iterator nhỏ | Rất cao khi có Write | Rất thấp (O(1)) | Thấp |
| Khả năng xóa | iterator.remove() | 🔴 Cấm (gây crash) | 🔴 Không hỗ trợ remove | Có hỗ trợ | Tùy triển khai |
| Xử lý đa luồng | 🔴 Không đồng bộ | 🔴 Không đồng bộ | 🟢 An toàn tuyệt đối | 🟢 An toàn tuyệt đối | 🚀 Tối ưu tính toán song song |
# tổng kết
Iterator Pattern đã tiến hóa một chặng đường dài trong hệ sinh thái Java:
- Từ GoF Iterator ban đầu nhằm che giấu cấu trúc dữ liệu.
- Đến cơ chế bảo vệ tính toàn vẹn dữ liệu Fail-Fast với
modCount. - Giải quyết bài toán concurrency với Fail-Safe và Weakly Consistent.
- Bùng nổ hiệu năng đa lõi với
Spliteratortrong Java 8. - Và trở thành công cụ đắc lực để Streaming dữ liệu cực lớn thông qua Cursor Iterator.
Hiểu rõ bản chất hoạt động bên dưới của các cơ chế này là lằn ranh phân biệt giữa một lập trình viên chỉ biết dùng vòng lặp và một Kiến trúc sư Hệ thống làm chủ toàn bộ tài nguyên CPU và bộ nhớ.
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.
On this page
- # cơ chế fail-fast và bí ẩn biến modCount trong openjdk
- # tại sao điều này xảy ra?
- # bản chất của modCount
- # fail-fast vs fail-safe / weakly consistent trong concurrency
- # CopyOnWriteArrayList (fail-safe via snapshot)
- # ConcurrentHashMap (weakly consistent)
- # cuộc tiến hóa lên Spliterator: mở khóa parallel streams (Java 8+)
- # các phương thức cốt lõi của Spliterator
- # xây dựng cursor streaming Iterator: Stream 10 triệu bản ghi không tràn ram
- # giải pháp: triển khai Keyset Pagination Iterator (streaming cursor)
- # triển khai mã nguồn chuẩn enterprise
- # sử dụng trong pipeline export dữ liệu
- # ma trận đánh giá so sánh các cơ chế duyệt trong Java
- # tổng kết