Konkurrierender Zugriff auf Container-Datenstrukturen

Parallele Programmierung (3IB)

Prof. Dr.-Ing. Sandro Leuchter
Hochschule Mannheim, Fakultät für Informatik
Wintersemester 2024/2025

 

Dieses Werk ist lizenziert unter einer Creative Commons „Namensnennung – Nicht-kommerziell – Weitergabe unter gleichen Bedingungen 4.0 International“ Lizenz.

Überblick

=> Grundlage: Hettel und Tran (2016, Kap. 9)

  • Threadsicherheit von Containern
    • Vector, Stack, HashTable und Dictionary
    • java.util.Collection-Implementierungen
    • java.util.Collections.synchronizedXXX
    • speziell: Itereration
  • “Hand-Over-Hand Locking” bei verketteten Listen
  • Experimentaldesign

Container-Typen

Threadsicherheit von Containern

  • Vector, Stack, HashTable und Dictionary
    • Alle öffentlichen Methoden sind synchronized.
    • Locking unnötiger Overhead in Single-Threadumgebungen
    • außerdem gibt es effizientere Methoden (vgl. ReadWriteLock*)
  • java.util.Collection-Implementierungen
    • sind deshalb nicht synchronisiert
    • Stattdessen gibt es Wrapper-Klassen, die die nicht threadsicheren Collection-Implementierungen kapseln:
        List<Person> listUnsafe = new ArrayList<>();
        List<Person> listSafe = Collections.synchronizedList(listUnsafe);

Collection Interfaces

Listenimplementierungen

bei ArrayList:

  • das am Ende Einfügen ist sehr unaufwändig (O(1)), außer wenn das Array bereits voll ist. Dann muss mehr Speicher alloziiert werdenund die Elemente umkopiert werden (O(N))
  • Einfügen in der Mitte \(\to\) alle folgenden müssen verschoben werden (O(N))

Hettel und Tran (2016, 126, linker Teil)

Listenimplementierungen

bei LinkedList:

  • das am Ende Einfügen erfordert Itereration über die ganze Liste (O(N))
  • Einfügen in der Mitte \(\to\) Iteration bis dorthin, dann einfügen (O(N))

Hettel und Tran (2016, 126, rechter Teil)

java.util.Collections.synchronizedXXX

Jeder Zugriff auf eine durch synchronizedXXX erzeugte Collection/Map wird durch synchronized(this) geschützt.

  • nicht sehr effizient
  • immer die ganze Collection (oft nicht nötig)
  • keine Unterscheidung zwischen Lesen und Ändern
  • nur einzelne Methode ist synchronized, Problem bei “Transaktion” (z.B. Iterieration über Collection)

Laboraufgabe “SynchronizedCollection-Wrapper”

  • Projekt: pp.07.01-synchronizedWrapper
  • Bearbeitungszeit: 20 Minuten
  • Musterlösung: 10 Minuten
  • Kompatibilität: mindestens Java SE 10

Threadsichere Iteration

Problem beim Iterieren

for (var i = 0; i < list.size(); i++) {/*...*/ list.get(i) /*...*/}
  • nebenläufig Element aus list entfernen
    • \(\to\) IndexOutOfBoundsException (am Ende)
  • nebenläufig Element zu list hinzufügen
    • \(\to\) keine Iteration über alle Elemente
  • nebenläufig Element aus list ändern
    • \(\to\) funktioniert
for (var p : list) {/*...*/}

… identisch mit…

var it = list.iterator();
while (it.hasNext()) {
    var p = it.next();
    // ...
}

nebenläufig Element aus list ändern/entfernen oder zu list hinzufügen
\(\to\) ConcurrentModificationException

Lösung für threadsicheres Iterieren

Durch synchronizedCollection ist nur jeder einzelne isolierte Zugriff geschützt.

Beim Iterieren muss daher in der Regel ein “äußerer Lock” benutzt werden:

var listUnsafe = new ArrayList<Type>();
var listSafe = Collections.synchronizedCollection(listUnsafe);

synchronized (listSafe) {
    for (var e : listSafe) {
        f(e);
    }
}

Threadsicher über HashMap iterieren

var mapUnsafe = new HashMap<KeyType, ValType>();
var mapSafe = Collections.synchronizedMap(mapUnsafe);
synchronized (mapSafe) {
    for (var k : mapSafe.keySet()) {
        f(mapSafe.get(k));
    }
}

Selektives Locking eines Containers

Optimierung des Locking einer verketteten Liste durch “Hand-over-Hand Locking”

  • Hier wird immer nur das eine Element gelockt, das gerade untersucht wird (1).
  • Beim Schritt zum nächsten Element wird kurzfristig das nächste Element mitgelockt (1,2),
  • dann kann der vorige Lock gelöst werden (2),
  • Beim Schritt zum nächsten Element wird kurzfristig das nächste Element mitgelockt (2,3),
  • dann kann der vorige Lock gelöst werden (3),
  • Beim Schritt zum nächsten Element wird kurzfristig das nächste Element mitgelockt (3,4). Nun ist die richtige Position in der Liste erreicht und das neue Element kann zwischen (3,4) eingefügt werden.

Butcher (2014, 25)

Experimentaldesign

Planung von Experimenten

  1. Hypothese über Wirkzusammenhänge (postulieren von Abhängigkeiten zwischen Elementen der Situation)
  2. Planung, wie die Hypothese experimentell untersucht werden kann
  3. aus protokollierten Beobachtungen Rückschlüsse auf Hypothese ziehen

unabhängige Variable (UV)

Einflussgröße der Situation, die mehrere Ausprägungen hat.

abhängige Variable (AV)

Messgröße, die man beobachten kann und von der man Änderungen erwartet, wenn UV variiert werden

Experiment

systematische Variation der UV bei gleichzeitiger Beobachtung der Auswirkung auf die AV

Laboraufgabe “Experimenteller Vergleich von Container-Varianten”

  • Projekt: pp.07.02.Collections
  • Bearbeitungszeit: 30 Minuten
  • Musterlösung: 30 Minuten
  • Kompatibilität: mindestens Java SE 10

Referenzen

Butcher, Paul. 2014. Seven Concurrency Models in Seven Weeks. When Threads Unravel. Dallas: The Pragmatic Programmers.
Hettel, Jörg und Manh Tien Tran. 2016. Nebenläufige Programmierung mit Java. Konzepte und Programmiermodelle für Multicore-Systeme. Heildelberg: dpunkt.verlag.