Reading Time: 9 minutes

Trong quá trình phát triển ứng dụng Java, đặc biệt là khi làm việc với các Collection (như ArrayList, HashMap…), chắc hẳn bạn đã không ít lần giật mình với một ngoại lệ quen thuộc nhưng cũng đầy phiền toáijava.util.ConcurrentModificationException.

concurrentmodificationexception trong java

Đây không chỉ là một lỗi cú pháp đơn thuần mà còn là một dấu hiệu cho thấy có điều gì có nhiều vấn đề đang xảy ra với dữ liệu của bạn, có thể dẫn đến những hành vi khó lường trong ứng dụng.

Hãy cùng tôi đi sâu vào chi tiết để biến nỗi lo về ConcurrentModificationException thành sự tự tin trong mỗi dòng code bạn viết.

1java.util.ConcurrentModificationException là gì và Tại sao nó xảy ra?

Như đã đề cập, java.util.ConcurrentModificationException là một ngoại lệ rất phổ biến khi làm việc với các lớp Collection trong Java. Các lớp Collection chuẩn của Java (như ArrayList, HashMap, HashSet…) được thiết kế theo cơ chế “fail-fast”. Điều này có nghĩa là nếu một Collection bị thay đổi cấu trúc (thêm, xóa phần tử, hoặc thay đổi kích thước) trong khi một luồng nào đó đang duyệt qua nó bằng cách sử dụng Iterator, thì ngay lập tức phương thức iterator.next() sẽ ném ra ngoại lệ ConcurrentModificationException.

Điều đáng chú ý là ngoại lệ này có thể xảy ra trong cả môi trường lập trình Java đa luồng (multithreaded) lẫn đơn luồng (single-threaded).

Để hiểu rõ hơn, hãy xem xét ví dụ minh họa dưới đây:

package com.journaldev.ConcurrentModificationException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class ConcurrentModificationExceptionExample {

	public static void main(String args[]) {
		List<String> myList = new ArrayList<String>();

		myList.add("1");
		myList.add("2");
		myList.add("3");
		myList.add("4");
		myList.add("5");

		// Phần List: Gây ra ConcurrentModificationException
		Iterator<String> it = myList.iterator();
		while (it.hasNext()) {
			String value = it.next();
			System.out.println("List Value:" + value);
			if (value.equals("3"))
				myList.remove(value); // Thay đổi cấu trúc của List khi đang duyệt
		}

		System.out.println("\\\\n--- Phần xử lý Map ---");
		Map<String, String> myMap = new HashMap<String, String>();
		myMap.put("1", "1");
		myMap.put("2", "2");
		myMap.put("3", "3");

		// Phần Map: Có thể không gây ra ConcurrentModificationException (tùy trường hợp)
		Iterator<String> it1 = myMap.keySet().iterator();
		while (it1.hasNext()) {
			String key = it1.next();
			System.out.println("Map Value:" + myMap.get(key));
			if (key.equals("2")) {
				myMap.put("1", "4"); // Chỉ cập nhật giá trị của key đã tồn tại
				// myMap.put("4", "4"); // Uncomment dòng này để gây ra ConcurrentModificationException
			}
		}

	}
}

Khi chạy chương trình trên, bạn sẽ thấy nó ném ra java.util.ConcurrentModificationException ngay lập tức, như trong log console dưới đây:

List Value:1
List Value:2
List Value:3
Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:937)
	at java.base/java.util.ArrayList$Itr.next(ArrayList.java:891)
	at com.journaldev.ConcurrentModificationException.ConcurrentModificationExceptionExample.main(ConcurrentModificationExceptionExample.java:22)

Từ stack trace của lỗi, rõ ràng ngoại lệ ConcurrentModificationException được ném ra khi chúng ta gọi phương thức iterator.next(). Nếu bạn đang thắc mắc làm thế nào Iterator kiểm tra được sự thay đổi, thì việc triển khai của nó nằm trong lớp AbstractList, nơi một biến int tên là modCount được định nghĩa. Biến modCount này theo dõi số lần kích thước hoặc cấu trúc của danh sách đã được thay đổi. Giá trị modCount được sử dụng trong mỗi lần gọi next() để kiểm tra xem có bất kỳ thay đổi nào trong hàm checkForComodification() hay không. Khi modCount của Iterator không khớp với modCount của Collection gốc ngoại lệ sẽ được ném ra.

Bây giờ, hãy thử comment phần xử lý List (từ dòng List<String> myList = new ArrayList<String>(); đến myList.remove(value);) và chạy lại chương trình. Bạn sẽ thấy không có ConcurrentModificationException nào được ném ra từ phần Map:

--- Phần xử lý Map ---
Map Value:3
Map Value:2
Map Value:4

Vì chúng ta chỉ cập nhật giá trị của một key đã tồn tại (myMap.put("1", "4")), kích thước của myMap không thay đổi, và do đó, chúng ta không gặp ConcurrentModificationException. Lưu ý rằng thứ tự xuất hiện của Map Value có thể khác trên hệ thống của bạn vì keySet của HashMap không được sắp xếp như List.

Tuy nhiên, nếu bạn bỏ comment dòng myMap.put("4", "4"); (tức là thêm một cặp key-value mới), nó sẽ làm thay đổi cấu trúc của HashMap và gây ra ConcurrentModificationException.

Vậy có giải pháp nào để tránh những cơn đau đầu này không?


2 Các Giải Pháp Hóa Giải ConcurrentModificationException

Chúng ta sẽ chia thành hai trường hợp chính: xử lý trong môi trường đa luồng và đơn luồng.

2.1. Để tránh ConcurrentModificationException trong môi trường đa luồng (Multi-threaded)

Khi nhiều luồng có thể truy cập và thay đổi một Collection cùng lúc, việc kiểm soát đồng bộ là vô cùng quan trọng.

  • Chuyển đổi Collection thành mảng và duyệt trên mảng: Bạn có thể chuyển đổi List thành một Array và sau đó duyệt qua mảng. Cách tiếp cận này hoạt động tốt cho các danh sách có kích thước nhỏ hoặc trung bình. Tuy nhiên, nếu danh sách rất lớn, việc tạo một bản sao (copy) của toàn bộ dữ liệu có thể ảnh hưởng nghiêm trọng đến hiệu suất (performance).
    List<String> myList = new ArrayList<>();
    // ... add elements ...
    String[] myArray = myList.toArray(new String[0]);
    for (String value : myArray) {
        // Có thể thay đổi myList ở đây mà không sợ lỗi, vì đang duyệt myArray
        System.out.println("Array Value:" + value);
    }
    
    
  • Khóa Collection khi đang duyệt bằng khối synchronized: Bạn có thể đặt Collection vào một khối synchronized để đảm bảo chỉ có một luồng được phép truy cập và thay đổi nó tại một thời điểm. Tuy nhiên, cách tiếp cận này không được khuyến nghị vì nó sẽ loại bỏ lợi ích của đa luồng bằng cách biến thao tác đồng bộ thành tuần tự, gây tắc nghẽn (bottleneck) và giảm hiệu suất đáng kể trong các ứng dụng đa luồng.
    List<String> myList = new ArrayList<>();
    // ... add elements ...
    synchronized (myList) {
        Iterator<String> it = myList.iterator();
        while (it.hasNext()) {
            String value = it.next();
            // myList.remove(value); // Thao tác thay đổi vẫn có thể gây lỗi nếu it.next() được gọi lại
            // Tốt nhất là không thay đổi cấu trúc khi đã synchronize và đang duyệt bằng iterator
        }
    }
    
    

    Lưu ý: Mặc dù synchronized giúp tránh thay đổi từ luồng khác, nhưng nếu cùng một luồng thay đổi Collection trong khi đang duyệt bằng iterator, ConcurrentModificationException vẫn có thể xảy ra. synchronized chủ yếu giải quyết vấn đề truy cập đồng thời từ nhiều luồng.

  • Sử dụng các lớp Concurrent của Java (JDK 1.5 trở lên): Đây là cách tiếp cận được khuyến nghị để tránh ConcurrentModificationException trong môi trường đa luồng. Từ JDK 1.5, gói java.util.concurrent đã cung cấp các lớp Collection được thiết kế đặc biệt để hoạt động an toàn trong môi trường đa luồng, như ConcurrentHashMapCopyOnWriteArrayList.

2.2. Để tránh ConcurrentModificationException trong môi trường đơn luồng (Single-threaded)

Ngay cả trong môi trường đơn luồng, bạn vẫn có thể gặp ConcurrentModificationException nếu thay đổi Collection trong khi đang duyệt. Dưới đây là các giải pháp:

  • Sử dụng phương thức iterator.remove(): Nếu bạn muốn xóa một đối tượng khỏi Collection trong khi đang duyệt bằng Iterator, hãy sử dụng phương thức iterator.remove() thay vì collection.remove(). Phương thức này được thiết kế để xóa phần tử cuối cùng được trả về bởi next() một cách an toàn mà không làm thay đổi modCount một cách bất ngờ đối với Iterator. Tuy nhiên, bạn chỉ có thể xóa phần tử hiện tại mà Iterator đang trỏ tới.
    Iterator<String> it = myList.iterator();
    while (it.hasNext()) {
        String value = it.next();
        if (value.equals("3")) {
            it.remove(); // An toàn để xóa phần tử hiện tại
        }
    }
    
    
  • Sử dụng các lớp Concurrent của Java: Các lớp Concurrent không chỉ hữu ích trong môi trường đa luồng mà còn có thể được sử dụng trong môi trường đơn luồng để đơn giản hóa việc quản lý các thay đổi đồng thời mà không gây lỗi.Hãy xem ví dụ sử dụng các lớp Collection Concurrent:
    package com.journaldev.ConcurrentModificationException;
    
    import java.util.Iterator;
    import java.util.List;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.CopyOnWriteArrayList;
    
    public class AvoidConcurrentModificationException {
    
    	public static void main(String[] args) {
    
    		System.out.println("--- Sử dụng CopyOnWriteArrayList ---");
    		List<String> myList = new CopyOnWriteArrayList<String>();
    
    		myList.add("1");
    		myList.add("2");
    		myList.add("3");
    		myList.add("4");
    		myList.add("5");
    
    		Iterator<String> it = myList.iterator();
    		while (it.hasNext()) {
    			String value = it.next();
    			System.out.println("List Value:" + value);
    			if (value.equals("3")) {
    				myList.remove("4"); // Thay đổi cấu trúc của List
    				myList.add("6");
    				myList.add("7");
    			}
    		}
    		System.out.println("List Size:" + myList.size());
    		System.out.println("List sau khi thay đổi:" + myList);
    
    		System.out.println("\\\\n--- Sử dụng ConcurrentHashMap ---");
    		Map<String, String> myMap = new ConcurrentHashMap<String, String>();
    		myMap.put("1", "1");
    		myMap.put("2", "2");
    		myMap.put("3", "3");
    
    		Iterator<String> it1 = myMap.keySet().iterator();
    		while (it1.hasNext()) {
    			String key = it1.next();
    			System.out.println("Map Value (Key: " + key + "):" + myMap.get(key));
    			if (key.equals("1")) {
    				myMap.remove("3"); // Thay đổi cấu trúc của Map
    				myMap.put("4", "4");
    				myMap.put("5", "5");
    			}
    		}
    
    		System.out.println("Map Size:" + myMap.size());
    		System.out.println("Map sau khi thay đổi:" + myMap);
    	}
    
    }
    
    

    Output của chương trình trên sẽ không ném ConcurrentModificationException:

    --- Sử dụng CopyOnWriteArrayList ---
    List Value:1
    List Value:2
    List Value:3
    List Value:4
    List Value:5
    List Size:6
    List sau khi thay đổi:[1, 2, 3, 5, 6, 7]
    
    --- Sử dụng ConcurrentHashMap ---
    Map Value (Key: 1):1
    Map Value (Key: 2):2
    Map Value (Key: 4):4
    Map Value (Key: 5):5
    Map Size:4
    Map sau khi thay đổi:{1=1, 2=2, 4=4, 5=5}
    
    

    Từ ví dụ trên, chúng ta có thể thấy rõ:

    • Các lớp Collection Concurrent có thể được sửa đổi một cách an toàn mà không ném ConcurrentModificationException.
    • Trong trường hợp của CopyOnWriteArrayList: Iterator của nó không tính đến các thay đổi mới trong danh sách và hoạt động trên một “snapshot” (bản sao) của danh sách gốc tại thời điểm Iterator được tạo ra. Do đó, các phần tử mới được thêm vào hoặc bị xóa sau khi Iterator được tạo sẽ không ảnh hưởng đến quá trình duyệt hiện tại.
    • Trong trường hợp của ConcurrentHashMap: hành vi có thể không luôn giống nhau một cách tuyệt đối, đặc biệt là với các phần tử mới được thêm vào.
      • Ví dụ, với điều kiện: Output đã cho thấy nó đã duyệt qua các đối tượng mới được thêm vào (Map Value (Key: 4):4, Map Value (Key: 5):5).
        if(key.equals("1")){
            myMap.remove("3");
            myMap.put("4", "4");
            myMap.put("5", "5");
        }
        
        
      • Tuy nhiên, hành vi này có thể thay đổi tùy thuộc vào cách ConcurrentHashMap tổ chức dữ liệu nội bộ và thứ tự duyệt của keySet (không có thứ tự). Do đó, nếu bạn sử dụng ConcurrentHashMap, hãy tránh thêm các đối tượng mới trong khi đang duyệt nếu bạn cần đảm bảo rằng các đối tượng mới đó sẽ được xử lý ngay lập tức trong cùng một lần duyệt. Việc xóa hoặc cập nhật các key hiện có thường ít vấn đề hơn.
  • Sử dụng vòng lặp for truyền thống để duyệt: Nếu bạn đang làm việc trong môi trường đơn luồng và muốn code của mình có thể xử lý các đối tượng được thêm vào danh sách trong khi duyệt, bạn có thể sử dụng vòng lặp for thay vì Iterator.
    List<String> myList = new ArrayList<>();
    myList.add("1");
    myList.add("2");
    myList.add("3");
    myList.add("4");
    myList.add("5");
    
    System.out.println("\\\\n--- Sử dụng for loop truyền thống ---");
    for(int i = 0; i < myList.size(); i++){
    	String value = myList.get(i);
    	System.out.println("List Value:" + value);
    	if(value.equals("3")){
    		myList.remove(i); // Xóa phần tử hiện tại
    		i--; // GIẢM CHỈ SỐ để đảm bảo không bỏ qua phần tử tiếp theo
    		myList.add("6"); // Thêm phần tử mới
    	}
    }
    System.out.println("List Size sau for loop:" + myList.size());
    System.out.println("List sau for loop:" + myList);
    
    

    Output:

    --- Sử dụng for loop truyền thống ---
    List Value:1
    List Value:2
    List Value:3
    List Value:4
    List Value:5
    List Size sau for loop:5
    List sau for loop:[1, 2, 4, 5, 6]
    
    

    Lưu ý rằng tôi đã giảm chỉ số i đi 1 (i--) bởi vì tôi đang xóa phần tử tại vị trí i. Nếu bạn không làm điều này, khi một phần tử bị xóa, các phần tử phía sau sẽ dịch lên, và vòng lặp sẽ bỏ qua phần tử ngay sau phần tử đã xóa. Hãy tự thử nghiệm để hiểu rõ hơn!

2.3. Một trường hợp đặc biệt: ConcurrentModificationException với subList

Một điều nữa mà bạn có thể gặp ConcurrentModificationException là khi bạn cố gắng sửa đổi cấu trúc của danh sách gốc (backing list) trong khi đang sử dụng subList.

Hãy xem ví dụ đơn giản này:

package com.journaldev.ConcurrentModificationException;

import java.util.ArrayList;
import java.util.List;

public class ConcurrentModificationExceptionWithArrayListSubList {

	public static void main(String[] args) {

		List<String> names = new ArrayList<>();
		names.add("Java");
		names.add("PHP");
		names.add("SQL");
		names.add("Angular 2");

		List<String> first2Names = names.subList(0, 2);

		System.out.println(names + " , " + first2Names);

		// Thay đổi giá trị của một phần tử (không thay đổi cấu trúc) -> OK
		names.set(1, "JavaScript");
		System.out.println(names + " , " + first2Names);

		// Thay đổi kích thước (cấu trúc) của list gốc -> Gây ra ConcurrentModificationException
		names.add("NodeJS");
		System.out.println(names + " , " + first2Names); // Dòng này sẽ ném ngoại lệ
	}
}

Output của chương trình trên là:

[Java, PHP, SQL, Angular 2] , [Java, PHP]
[Java, JavaScript, SQL, Angular 2] , [Java, JavaScript]
Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1282)
	at java.base/java.util.ArrayList$SubList.listIterator(ArrayList.java:1151)
	at java.base/java.util.AbstractList.listIterator(AbstractList.java:311)
	at java.base/java.util.ArrayList$SubList.iterator(ArrayList.java:1147)
	at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:465)
	at java.base/java.lang.String.valueOf(String.java:2801)
	at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
	at com.journaldev.ConcurrentModificationException.ConcurrentModificationExceptionWithArrayListSubList.main(ConcurrentModificationExceptionWithArrayListSubList.java:26)

Theo tài liệu của ArrayList.subList, các thay đổi cấu trúc (structural modifications) chỉ được phép thực hiện trên danh sách được trả về bởi phương thức subList. Tất cả các phương thức trên danh sách con (subList) trước tiên sẽ kiểm tra xem modCount thực tế của danh sách gốc (backing list) có bằng giá trị dự kiến của nó hay không. Nếu không, nó sẽ ném ra ConcurrentModificationException. Việc thêm một phần tử mới vào names (list gốc) đã làm thay đổi cấu trúc của nó, khiến subList không còn hợp lệ nữa.

Kết Luận

Qua bài blog này, chúng ta đã cùng nhau tìm hiểu sâu về java.util.ConcurrentModificationException một ngoại lệ phổ biến nhưng thường gây bối rối trong Java. Chúng ta đã nắm được các điểm mấu chốt sau:

  • Nguyên nhân: Ngoại lệ này xảy ra khi một Collection “fail-fast” bị thay đổi cấu trúc (thêm, xóa phần tử) trong khi đang được duyệt bởi Iterator, hoặc khi một subList trở nên không hợp lệ do thay đổi cấu trúc của list gốc.
  • Cơ chế: Iterator sử dụng biến modCount để phát hiện các thay đổi không mong muốn trong khi duyệt.
  • Giải pháp cho môi trường đa luồng:
    • Cách tốt nhất là sử dụng các Collection an toàn cho đa luồng như ConcurrentHashMapCopyOnWriteArrayList (từ JDK 1.5).
    • Tránh chuyển đổi sang mảng lớn hoặc sử dụng synchronized cho toàn bộ quá trình duyệt vì lý do hiệu suất.
  • Giải pháp cho môi trường đơn luồng:
    • Sử dụng iterator.remove() khi bạn muốn xóa phần tử hiện tại.
    • Sử dụng vòng lặp for truyền thống và điều chỉnh chỉ số i (i--) nếu bạn cần thêm hoặc xóa phần tử trong khi duyệt và muốn các thay đổi đó được phản ánh ngay lập tức.
    • Các Collection Concurrent cũng có thể được sử dụng để đơn giản hóa logic, mặc dù bạn cần lưu ý về hành vi của chúng đối với các phần tử mới được thêm vào (như trong ConcurrentHashMap).

Hy vọng bài viết này đã cung cấp cho bạn cái nhìn toàn diện và những công cụ cần thiết để xử lý ConcurrentModificationException một cách tự tin. Hãy thực hành và thử nghiệm với các ví dụ trên để củng cố kiến thức nhé.

0 Bình luận

Đăng nhập để thảo luận

Chuyên mục Hướng dẫn

Tổng hợp các bài viết hướng dẫn, nghiên cứu và phân tích chi tiết về kỹ thuật, các xu hướng công nghệ mới nhất dành cho lập trình viên.

Đăng ký nhận bản tin của chúng tôi

Hãy trở thành người nhận được các nội dung hữu ích của CyStack sớm nhất

Xem chính sách của chúng tôi Chính sách bảo mật.

Đăng ký nhận Newsletter

Nhận các nội dung hữu ích mới nhất