Fix java.util.ConcurrentModificationException Khi Sửa Collection Trong Lúc Duyệt

intermediate Java2026-03-18| Java 8+, mọi hệ điều hành (Windows / Linux / macOS), mọi framework sử dụng java.util collections (ArrayList, HashMap, HashSet, v.v.)

Error Message

java.util.ConcurrentModificationException
#java#collection#concurrency#exception#ConcurrentModificationException

Lỗi Xảy Ra Như Thế Nào

Bạn đang duyệt qua một collection và xóa (hoặc thêm) phần tử ngay trong vòng lặp — Java lập tức ném ra ngoại lệ:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
    at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)
    at com.example.MyClass.processItems(MyClass.java:24)

Chín trong mười trường hợp, nguyên nhân là vòng lặp for-each có gọi list.remove(), list.add(), hoặc map.put() trực tiếp trên collection đang được duyệt.

Nguyên Nhân Gốc Rễ

Hầu hết các collection trong Java — ArrayList, HashMap, HashSet — duy trì một bộ đếm nội bộ tên là modCount. Bộ đếm này tăng lên mỗi khi cấu trúc collection thay đổi (thêm, xóa, xóa toàn bộ).

Khi bắt đầu duyệt, iterator ghi lại giá trị bộ đếm đó. Mỗi lần gọi next(), nó kiểm tra: collection có thay đổi kể từ lúc bắt đầu không? Nếu có, ngoại lệ được ném ngay lập tức.

Đây là thiết kế fail-fast — thông báo lỗi rõ ràng còn hơn âm thầm bỏ sót phần tử hoặc rơi vào vòng lặp vô tận.

Ví dụ điển hình:

List<String> items = new ArrayList<>(List.of("a", "b", "c", "remove-me"));
for (String item : items) {
    if (item.equals("remove-me")) {
        items.remove(item); // ném ConcurrentModificationException
    }
}

Cách Sửa 1: Dùng remove() Của Iterator

Iterator có phương thức remove() riêng — đó mới là thứ bạn nên dùng. Nó cập nhật modCount nội bộ, nên phép kiểm tra sẽ qua mà không có vấn đề gì.

List<String> items = new ArrayList<>(List.of("a", "b", "c", "remove-me"));
Iterator<String> iterator = items.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    if (item.equals("remove-me")) {
        iterator.remove(); // an toàn
    }
}
System.out.println(items); // [a, b, c]

Hạn chế: Chỉ có thể xóa phần tử hiện tại. Việc thêm phần tử hoặc xóa phần tử khác trong lúc duyệt vẫn không thực hiện được theo cách này.

Cách Sửa 2: Dùng removeIf() (Java 8+)

Với những tình huống lọc đơn giản, removeIf() là lựa chọn tốt nhất. Một dòng, gọn gàng, không cần boilerplate:

List<String> items = new ArrayList<>(List.of("a", "b", "c", "remove-me"));
items.removeIf(item -> item.equals("remove-me"));
System.out.println(items); // [a, b, c]

Hoạt động với ArrayList, HashSet, LinkedList — bất kỳ Collection nào triển khai phương thức này. Việc quản lý iterator được xử lý nội bộ bên dưới.

Cách Sửa 3: Gom Thay Đổi, Áp Dụng Sau Vòng Lặp

Cần thêm phần tử, hoặc xử lý phức tạp hơn? Đừng động vào collection bên trong vòng lặp. Gom các thay đổi vào danh sách riêng, rồi áp dụng sau khi duyệt xong.

List<String> items = new ArrayList<>(List.of("a", "b", "c", "remove-me"));
List<String> toRemove = new ArrayList<>();

for (String item : items) {
    if (item.equals("remove-me")) {
        toRemove.add(item);
    }
}

items.removeAll(toRemove);
System.out.println(items); // [a, b, c]

Cách tương tự áp dụng cho việc thêm phần tử — gom vào danh sách toAdd rồi gọi items.addAll(toAdd) sau vòng lặp:

List<String> toAdd = new ArrayList<>();
for (String item : items) {
    if (item.startsWith("prefix-")) {
        toAdd.add(item + "-processed");
    }
}
items.addAll(toAdd);

Cách Sửa 4: Dùng CopyOnWriteArrayList Khi Truy Cập Đồng Thời

Nhiều thread cùng thao tác trên một danh sách? Đó là bài toán khác. CopyOnWriteArrayList từ java.util.concurrent được tạo ra chính xác cho tình huống này:

import java.util.concurrent.CopyOnWriteArrayList;

List<String> items = new CopyOnWriteArrayList<>(List.of("a", "b", "c", "remove-me"));

for (String item : items) {
    if (item.equals("remove-me")) {
        items.remove(item); // an toàn — duyệt trên bản sao snapshot
    }
}
System.out.println(items); // [a, b, c]

Đánh đổi: Mỗi lần ghi tạo ra một bản sao mới của mảng bên dưới. Với danh sách 10.000 phần tử và ghi thường xuyên, chi phí này tích lũy rất nhanh. Chỉ dùng khi thực sự có yêu cầu truy cập đồng thời — không phải chỉ cho tiện.

Sửa Với Map

HashMap ném ra cùng ngoại lệ trong cùng điều kiện. Cách sửa tương tự như với list — dùng entrySet().removeIf() hoặc iterator tường minh:

Map<String, Integer> scores = new HashMap<>();
scores.put("alice", 90);
scores.put("bob", 45);
scores.put("charlie", 30);

// Xóa các entry có điểm < 50
scores.entrySet().removeIf(entry -> entry.getValue() < 50);
System.out.println(scores); // {alice=90}

// Hoặc dùng iterator
Iterator<Map.Entry<String, Integer>> it = scores.entrySet().iterator();
while (it.hasNext()) {
    Map.Entry<String, Integer> entry = it.next();
    if (entry.getValue() < 50) {
        it.remove();
    }
}

Kiểm Tra Kết Quả

Chạy lại code và kiểm tra ba điều:

  • Không còn ConcurrentModificationException trong stack trace.
  • Collection chứa đúng các phần tử mong muốn sau vòng lặp.
  • In ra để xác nhận: System.out.println(items);

Với code đa luồng, hãy thêm test chạy nhiều thread cùng lúc tác động vào danh sách. Một câu lệnh assert đơn giản là đủ để kiểm tra nhanh:

// Chạy và kỳ vọng không có ngoại lệ
List<String> result = new ArrayList<>(List.of("keep", "remove", "keep2"));
result.removeIf(s -> s.equals("remove"));
assert result.equals(List.of("keep", "keep2")) : "Kết quả không như mong đợi: " + result;

Nên Chọn Cách Nào?

  • Chỉ xóa phần tử: removeIf() — ít code nhất, ý định rõ ràng nhất.
  • Logic tùy chỉnh cho từng phần tử: Iterator.remove().
  • Thêm phần tử hoặc thay đổi phức tạp: Gom thay đổi vào danh sách riêng, áp dụng sau vòng lặp.
  • Thực sự đa luồng: CopyOnWriteArrayList — hoặc đồng bộ hóa bên ngoài nếu cần kiểm soát chi tiết hơn.

Related Error Notes