java.util.ConcurrentModificationException

java.util.ConcurrentModificationException è un’eccezione molto comune quando si lavora con le classi di raccolta di Java. Le classi di raccolta di Java sono “fail-fast”, il che significa che se la raccolta viene modificata mentre un thread la sta attraversando utilizzando l’iteratore, il metodo iterator.next() genererà una ConcurrentModificationException. L’eccezione di modifica concorrente può verificarsi sia in un ambiente di programmazione multithread che in un ambiente di programmazione Java single-threaded.

java.util.ConcurrentModificationException

Vediamo uno scenario di eccezione di modifica concorrente con un esempio.

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 myList = new ArrayList();

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

		Iterator it = myList.iterator();
		while (it.hasNext()) {
			String value = it.next();
			System.out.println("List Value:" + value);
			if (value.equals("3"))
				myList.remove(value);
		}

		Map myMap = new HashMap();
		myMap.put("1", "1");
		myMap.put("2", "2");
		myMap.put("3", "3");

		Iterator 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");
				// myMap.put("4", "4");
			}
		}

	}
}

Il programma sopra genererà l’eccezione java.util.ConcurrentModificationException quando viene eseguito, come mostrato nei log di console seguenti.

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)

Dall’output dello stack trace, è chiaro che l’eccezione di modifica concorrente viene lanciata quando chiamiamo la funzione iterator next(). Se ti stai chiedendo come l’iteratore controlla la modifica, l’implementazione è presente nella classe AbstractList, dove viene definita una variabile int modCount. Il valore di modCount fornisce il numero di volte in cui la dimensione della lista è stata modificata. Il valore di modCount viene utilizzato in ogni chiamata a next() per controllare eventuali modifiche nella funzione checkForComodification(). Ora, commenta la parte della lista e esegui nuovamente il programma. Vedrai che ora non viene lanciata alcuna ConcurrentModificationException. Output:

Map Value:3
Map Value:2
Map Value:4

Dato che stiamo aggiornando il valore della chiave esistente nella mia mappa, la sua dimensione non è stata modificata e non otteniamo ConcurrentModificationException. L’output potrebbe essere diverso nel tuo sistema perché il set di chiavi di HashMap non è ordinato come una lista. Se rimuoverai il commento dalla dichiarazione in cui sto aggiungendo una nuova chiave-valore alla HashMap, verrà lanciata una ConcurrentModificationException.

Per evitare ConcurrentModificationException in un ambiente multithread

  1. Puoi convertire la lista in un array e quindi iterare sull’array. Questo approccio funziona bene per liste di piccole o medie dimensioni, ma se la lista è grande avrà un forte impatto sulle prestazioni.
  2. È possibile bloccare la lista durante l’iterazione mettendola in un blocco sincronizzato. Questo approccio non è consigliato perché eliminerà i vantaggi del multithreading.
  3. Se stai usando JDK1.5 o versioni successive, puoi utilizzare le classi ConcurrentHashMap e CopyOnWriteArrayList. Questo è l’approccio consigliato per evitare l’eccezione di modifica concorrente.

Per evitare ConcurrentModificationException in un ambiente single-threaded

Puoi utilizzare la funzione remove() dell’iteratore per rimuovere l’oggetto dall’oggetto di raccolta sottostante. Ma in questo caso, puoi rimuovere lo stesso oggetto e non un altro oggetto dalla lista. Eseguiamo un esempio utilizzando le classi di raccolta concorrente.

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) {

		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");
				myList.add("6");
				myList.add("7");
			}
		}
		System.out.println("List Size:" + myList.size());

		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:" + myMap.get(key));
			if (key.equals("1")) {
				myMap.remove("3");
				myMap.put("4", "4");
				myMap.put("5", "5");
			}
		}

		System.out.println("Map Size:" + myMap.size());
	}

}

L’output del programma precedente è mostrato di seguito. Puoi vedere che il programma non genera alcuna ConcurrentModificationException.

List Value:1
List Value:2
List Value:3
List Value:4
List Value:5
List Size:6
Map Value:1
Map Value:2
Map Value:4
Map Value:5
Map Size:4

Dall’esempio precedente è chiaro che:

  1. Le classi di raccolta concorrente possono essere modificate in modo sicuro, non generano ConcurrentModificationException.

  2. Nel caso di CopyOnWriteArrayList, l’iteratore non si adatta alle modifiche nella lista e lavora sulla lista originale.

  3. Nel caso di ConcurrentHashMap, il comportamento non è sempre lo stesso. Per condizione:

    if(key.equals("1")){
    	myMap.remove("3");}
    

    L’output è:

    Valore Mappa:1
    Valore Mappa:null
    Valore Mappa:4
    Valore Mappa:2
    Dimensione Mappa:4
    

    Sta prendendo il nuovo oggetto aggiunto con la chiave “4” ma non il successivo oggetto aggiunto con la chiave “5”. Ora se cambio la condizione in basso.

    if(key.equals("3")){
    	myMap.remove("2");}
    

    L’output è:

    Valore Mappa:1
    Valore Mappa:3
    Valore Mappa:null
    Dimensione Mappa:4
    

    In questo caso, non sta considerando i nuovi oggetti aggiunti. Quindi, se stai usando ConcurrentHashMap, evita di aggiungere nuovi oggetti in quanto possono essere elaborati a seconda del keyset. Nota che lo stesso programma può stampare valori diversi nel tuo sistema perché l’insieme di chiavi HashMap non è ordinato.

Utilizzare il ciclo for per evitare la java.util.ConcurrentModificationException

Se stai lavorando in un ambiente single-threaded e desideri che il tuo codice si prenda cura degli oggetti aggiunti in più nella lista, puoi farlo utilizzando il ciclo for anziché un Iterator.

for(int i = 0; i<myList.size(); i++){
	System.out.println(myList.get(i));
	if(myList.get(i).equals("3")){
		myList.remove(i);
		i--;
		myList.add("6");
	}
}

Nota che sto diminuendo il contatore perché sto rimuovendo lo stesso oggetto, se devi rimuovere l’oggetto successivo o più lontano, non è necessario diminuire il contatore. Provalo tu stesso. 🙂 Un’altra cosa: Otterrai una ConcurrentModificationException se cercherai di modificare la struttura della lista originale con subList. Vediamo questo con un esempio semplice.

package com.journaldev.ConcurrentModificationException;

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

public class ConcurrentModificationExceptionWithArrayListSubList {

	public static void main(String[] args) {

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

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

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

		names.set(1, "JavaScript");
		// controlla l'output qui sotto. :)
		System.out.println(names + " , " + first2Names);

		// Modifichiamo la dimensione della lista e otteniamo la ConcurrentModificationException
		names.add("NodeJS");
		System.out.println(names + " , " + first2Names); // this line throws exception

	}

}

L’output del programma precedente è:

[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)

Secondo la documentazione di ArrayList su subList, le modifiche strutturali sono consentite solo sulla lista restituita dal metodo subList. Tutti i metodi sulla lista restituita prima controllano se il modCount effettivo della lista sottostante è uguale al suo valore atteso e generano una ConcurrentModificationException se non lo è.

È possibile scaricare tutto il codice di esempio dal nostro Repository GitHub.

Source:
https://www.digitalocean.com/community/tutorials/java-util-concurrentmodificationexception