How to Avoid Concurrent Modification Exceptions in Java Collections
Java collections are powerful tools for managing data, but they can introduce complexity, especially when dealing with concurrent modifications. One of the most common runtime exceptions developers encounter is ConcurrentModificationException
. This error arises when a collection is modified while it is being iterated, and it can be challenging to resolve without understanding its underlying causes. In this article, we’ll dive into the nature of this exception, common scenarios where it occurs, and effective strategies to avoid it.
1. What is ConcurrentModificationException
?
ConcurrentModificationException
is thrown when a collection is structurally modified while being iterated by an iterator, except when the modification is done through the iterator’s own remove
method. This structural modification includes actions like adding, removing, or changing elements that affect the size of the collection.
Example:
List<String> list = new ArrayList<>(); list.add("A"); list.add("B"); for (String s : list) { if (s.equals("A")) { list.remove(s); // Throws ConcurrentModificationException } }
Why it happens: In the above example, modifying the list while iterating over it triggers the exception because the list’s internal state is altered during the iteration, and the iterator is not aware of the change.
2. Common Scenarios Leading to ConcurrentModificationException
1. Modifying Collections in Enhanced For-Loops
One of the most frequent cases where this exception occurs is when using enhanced for
loops to iterate over collections and modifying them directly inside the loop.
Example:
List<Integer> numbers = new ArrayList<>(); numbers.add(1); numbers.add(2); numbers.add(3); for (Integer number : numbers) { if (number == 2) { numbers.remove(number); // Throws ConcurrentModificationException } }
Solution: Use an explicit iterator to modify the collection safely.
Iterator<Integer> iterator = numbers.iterator(); while (iterator.hasNext()) { Integer number = iterator.next(); if (number == 2) { iterator.remove(); // Safe removal using iterator } }
2. Modifying Collections While Iterating Over Multiple Threads
When multiple threads are accessing and modifying a shared collection, this can also lead to ConcurrentModificationException
.
Example:
List<String> sharedList = new ArrayList<>(); sharedList.add("A"); sharedList.add("B"); Runnable modifyTask = () -> { for (String s : sharedList) { if (s.equals("A")) { sharedList.remove(s); // May throw ConcurrentModificationException } } }; new Thread(modifyTask).start(); new Thread(modifyTask).start();
Solution: Use thread-safe collections like CopyOnWriteArrayList
or synchronized blocks to handle modifications safely across multiple threads.
List<String> safeList = new CopyOnWriteArrayList<>(); safeList.add("A"); safeList.add("B"); Runnable modifyTask = () -> { for (String s : safeList) { if (s.equals("A")) { safeList.remove(s); // No ConcurrentModificationException with CopyOnWriteArrayList } } }; new Thread(modifyTask).start(); new Thread(modifyTask).start();
3. Modifying Maps During Iteration
ConcurrentModificationException
is not limited to lists. It also occurs when modifying maps during iteration.
Example:
Map<Integer, String> map = new HashMap<>(); map.put(1, "A"); map.put(2, "B"); for (Map.Entry<Integer, String> entry : map.entrySet()) { if (entry.getKey() == 1) { map.remove(entry.getKey()); // Throws ConcurrentModificationException } }
Solution: Use the iterator’s remove
method or opt for ConcurrentHashMap
for thread-safe operations.
Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<Integer, String> entry = iterator.next(); if (entry.getKey() == 1) { iterator.remove(); // Safe removal using iterator } }
3. Strategies to Avoid ConcurrentModificationException
1. Use Iterator’s remove()
Method
If you need to remove elements during iteration, the safest way is to use the iterator’s remove()
method. This ensures that the iterator is aware of the structural modification and updates its internal state accordingly.
Example:
List<String> list = new ArrayList<>(); list.add("A"); list.add("B"); list.add("C"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String value = iterator.next(); if (value.equals("B")) { iterator.remove(); // Safe removal } }
2. Switch to Concurrent
Collections
For multi-threaded environments, use thread-safe alternatives from the java.util.concurrent
package. These collections, like ConcurrentHashMap
and CopyOnWriteArrayList
, are designed to handle concurrent modifications without throwing exceptions.
Example with ConcurrentHashMap
:
ConcurrentMap<Integer, String> map = new ConcurrentHashMap<>(); map.put(1, "A"); map.put(2, "B"); map.forEach((key, value) -> { if (key == 1) { map.remove(key); // Safe removal in ConcurrentHashMap } });
Example with CopyOnWriteArrayList
:
List<String> list = new CopyOnWriteArrayList<>(); list.add("A"); list.add("B"); for (String s : list) { if (s.equals("A")) { list.remove(s); // Safe removal in CopyOnWriteArrayList } }
3. Use Synchronized Blocks for Manual Synchronization
In cases where concurrent collections are not an option, and you’re dealing with multiple threads, manually synchronizing the block of code that modifies the collection can help prevent exceptions.
Example:
List<String> list = new ArrayList<>(); list.add("A"); list.add("B"); synchronized (list) { for (String s : list) { if (s.equals("A")) { list.remove(s); // Safe removal inside synchronized block } } }
4. Iterate Over a Copy of the Collection
Another approach is to iterate over a copy of the collection while modifying the original. This avoids modifying the collection while iterating over it, thus preventing the exception.
Example:
List<String> list = new ArrayList<>(); list.add("A"); list.add("B"); for (String s : new ArrayList<>(list)) { if (s.equals("A")) { list.remove(s); // Safe removal from the original list } }
4. Conclusion
ConcurrentModificationException
is a common issue when modifying Java collections during iteration, but it’s easily preventable with the right strategies. Whether you use an explicit iterator, switch to concurrent collections, or implement manual synchronization, understanding how Java handles structural modifications can save you from runtime surprises.