Collections und Nebenläufigkeit

ConcurrentModificationException

  • Collections und Maps sind fail-fast: Sie entdecken konkurrierende Zugriffe und werfen eine Ausnahme (ConcurrentModification Exception) wenn eine solche vorkommt
  • Daher darf man während einer Iteration nur über den Iterator die Collection verändern, nicht aber am Iterator vorbei
  • Direkte Änderungen während des Iterierens sind verboten
  • Man sollte aber die ConcurrentModificationException nicht fangen und sich nicht darauf verlassen, dass sie geworfen wird

Ein Iterator, der über eine Collection läuft, stellt eine Momentaufnahme der Daten in dem Collection-Objekt dar. Wenn die Collection während der Iteration verändert würde, könnte es zu ungültigen Zuständen kommen. Man stelle sich z. B. vor, der Iterator wollte ein Objekt zurückgeben, dass genau in diesem Augenblick aus der Collection entfernt wird. Oder es wird hinter der aktuellen Iterator-Position ein Objekt in die Collection eingefügt.

Aus diesem Grund ist das Verändern der Collection während der Iteration grundsätzlich verboten. Die Collection-Klassen werfen daher eine Ausnahme, wenn dies trotzdem versucht wird.

Für die nebenläufige Programmierung existieren spezielle Collection-Klassen, die parallele Veränderungen und Lesevorgänge zulassen. Diese werden später noch vorgestellt.

Beispiel: ConcurrentModificationException

List<String> list = new ArrayList<>();
Collections.addAll(list, "A", "B", "C", "D", "E", "X", "F");

for (String element : list) {
    if ("X".equals(element)) {
        list.remove(element); // FEHLER!!
    }
}
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.AbstractList$Itr.checkForComodification(
          AbstractList.java:372)
    at java.util.AbstractList$Itr.next(AbstractList.java:343)
    at pr2.CMEDemo.main(ConcurrentModificationException.java:14)

Das Entfernen des Elements aus der Collection während der Iteration durch list.remove(element) führt zu einer Ausnahme.

Der im Beispiel gewählte Ansatz ist sowieso nicht sinnvoll, da alle Collections eine Methode remove haben, mit der man ohne Verwendung eines Iterators Objekte aus der Collection entfernen kann.

Thread-Safety

  • Die modernen Collection Implementierungen (ArrayList, LinkedList, HashSet, TreeSet, HashMap) sind nicht thread-safe
  • Die alten Implementierungen (Vector, Hashtable) sind thread-safe, da alle Methoden synchronisiert sind
  • Man kann jedes Collection-Objekt mit Collections.synchronized... in ein threadsicheres umwandeln

Wenn eine Collection von mehreren Threads gleichzeitig benutzt werden soll, muss sie threadsicher (thread-safe) sein. Wie im Kapitel zu Threads erläutert wird, müssen die Methoden mit synchronized als kritische Abschnitte definiert werden. Im alten Collection-API sind alle Methoden entsprechend markiert, im neuen nicht. Der Grund liegt darin, dass der Aufruf einer synchronisierten Methode aufwändiger ist als der einer nicht-synchronisierten. Da die meisten Collections von nur einem Thread benutzt werden, wäre es unnötig die Methoden grundsätzlich zu synchronisieren.

Benötigt man eine threadsichere Collection, kann man diese einfach über Klasse Collections anfordern. Dort gibt es für alle Interfaces eine Methode sychronized... (z. B. synchronizedSet), die eine threadsichere Variante der übergebenen Collection zurückliefert.

ConcurrentHashMap<K, V>

ConcurrentHashMap

  • Verwendet deutlich bessere Locking-Strategie als Collections.synchronizedMap(...)
  • Iteratoren werfen keine ConcurrentModificationException
  • Besitzt zusätzliche Methoden für das check-then-act-Pattern, um externe Synchronisation zu vermeiden

Methoden

  • V putIfAbsent(K key, V value) – Fügt value nur hinzu, wenn für den Schlüssel k noch kein Eintrag vorhanden ist
  • boolean remove(Object key, Object value) – Entfernt den Schlüssel key nur dann, wenn er mit dem gegebenen Wert value verbunden ist
  • boolean replace(K key, V oldValue, V newValue) – Ersetzt den Wert für den Schlüssel key nur wenn key vorhanden ist und auf oldValue zeigt
  • V replace(K key, V value) – Ersetzt den Wert value für den Schlüssel key nur dann, wenn key bereits vorhanden ist

Beispiel: ConcurrentHashMap

if (!map.containsKey(key)) {
    map.put(key, value);
}
else {
    value = map.get(key);
}


value = map.putIfAbsent(key, value);

CopyOnWriteArrayList<E>

CopyOnWriteArrayList

  • Optimierte Liste für Szenarien, in denen viele Threads von einer Liste lesen, sie aber nur selten geschrieben wird
  • Normale Synchronisation würde hier erhebliche Geschwindigkeitseinbussen bedeuten
  • Bei jeder Schreiboperation wird eine Kopie der Liste angelegt
  • Iteratoren werfen keine ConcurrentModificationException

Copyright © 2025 Thomas Smits