Eine Einführung in synchronisierte Java-Sammlungen

1. Übersicht

Das Sammlungsframework ist eine Schlüsselkomponente von Java. Es bietet eine große Anzahl von Schnittstellen und Implementierungen, mit denen wir auf einfache Weise verschiedene Arten von Sammlungen erstellen und bearbeiten können.

Obwohl die Verwendung einfacher nicht synchronisierter Sammlungen insgesamt einfach ist, kann sie auch bei der Arbeit in Umgebungen mit mehreren Threads (auch als gleichzeitige Programmierung bezeichnet) zu einem entmutigenden und fehleranfälligen Prozess werden.

Daher bietet die Java-Plattform eine starke Unterstützung für dieses Szenario durch verschiedene Synchronisations- Wrapper, die in der Collections- Klasse implementiert sind .

Diese Wrapper erleichtern das Erstellen synchronisierter Ansichten der bereitgestellten Sammlungen mithilfe verschiedener statischer Factory-Methoden.

In diesem Tutorial werden wir uns eingehend mit diesen statischen Synchronisations-Wrappern befassen. Außerdem werden wir den Unterschied zwischen synchronisierten Sammlungen und gleichzeitigen Sammlungen hervorheben .

2. Die synchronizedCollection () -Methode

Der erste Synchronisations-Wrapper, den wir in dieser Zusammenfassung behandeln werden, ist die synchronizedCollection () -Methode. Wie der Name schon sagt, gibt es eine Thread-sichere Sammlung von der angegebenen gesichert Sammlung .

Um besser zu verstehen, wie diese Methode verwendet wird, erstellen wir einen grundlegenden Komponententest:

Collection syncCollection = Collections.synchronizedCollection(new ArrayList()); Runnable listOperations = () -> { syncCollection.addAll(Arrays.asList(1, 2, 3, 4, 5, 6)); }; Thread thread1 = new Thread(listOperations); Thread thread2 = new Thread(listOperations); thread1.start(); thread2.start(); thread1.join(); thread2.join(); assertThat(syncCollection.size()).isEqualTo(12); } 

Wie oben gezeigt, ist das Erstellen einer synchronisierten Ansicht der bereitgestellten Sammlung mit dieser Methode sehr einfach.

Um zu demonstrieren, dass die Methode tatsächlich eine thread-sichere Sammlung zurückgibt, erstellen wir zunächst einige Threads.

Danach fügen wir eine Runnable- Instanz in Form eines Lambda-Ausdrucks in ihre Konstruktoren ein. Denken wir daran, dass Runnable eine funktionale Schnittstelle ist, sodass wir sie durch einen Lambda-Ausdruck ersetzen können.

Zuletzt überprüfen wir nur, ob jeder Thread der synchronisierten Sammlung effektiv sechs Elemente hinzufügt, sodass seine endgültige Größe zwölf beträgt.

3. Die synchronizedList () -Methode

Ähnlich wie bei der synchronizedCollection () -Methode können wir den synchronizedList () -Wrapper verwenden, um eine synchronisierte Liste zu erstellen .

Wie zu erwarten, gibt die Methode eine thread-sichere Ansicht der angegebenen Liste zurück :

List syncList = Collections.synchronizedList(new ArrayList());

Es überrascht nicht, dass die Verwendung der synchronizedList () -Methode nahezu identisch mit der übergeordneten Methode synchronizedCollection () ist .

Daher können wir, wie wir es gerade im vorherigen Komponententest getan haben, nach dem Erstellen einer synchronisierten Liste mehrere Threads erzeugen. Danach tun, werden wir sie den Zugriff auf / manipulieren , um die Ziel verwenden Liste in einem Thread-sichere Art und Weise.

Wenn wir eine synchronisierte Sammlung durchlaufen und unerwartete Ergebnisse verhindern möchten, sollten wir außerdem explizit unsere eigene thread-sichere Implementierung der Schleife bereitstellen. Daher könnten wir dies mit einem synchronisierten Block erreichen:

List syncCollection = Collections.synchronizedList(Arrays.asList("a", "b", "c")); List uppercasedCollection = new ArrayList(); Runnable listOperations = () -> { synchronized (syncCollection) { syncCollection.forEach((e) -> { uppercasedCollection.add(e.toUpperCase()); }); } }; 

In allen Fällen, in denen wir über eine synchronisierte Sammlung iterieren müssen, sollten wir diese Redewendung implementieren. Dies liegt daran, dass die Iteration einer synchronisierten Sammlung durch mehrere Aufrufe der Sammlung ausgeführt wird. Daher müssen sie als einzelne atomare Operation ausgeführt werden.

Die Verwendung des synchronisierten Blocks stellt die Atomizität der Operation sicher .

4. Die synchronizedMap () -Methode

Die Collections- Klasse implementiert einen weiteren übersichtlichen Synchronisations-Wrapper namens synchronizedMap (). Wir könnten es verwenden, um einfach eine synchronisierte Karte zu erstellen .

Die Methode gibt eine thread-sichere Ansicht der bereitgestellten Map- Implementierung zurück :

Map syncMap = Collections.synchronizedMap(new HashMap()); 

5. Die synchronizedSortedMap () -Methode

Es gibt auch eine Gegenstückimplementierung der synchronizedMap () -Methode. Es heißt synchronizedSortedMap () und kann zum Erstellen einer synchronisierten SortedMap- Instanz verwendet werden:

Map syncSortedMap = Collections.synchronizedSortedMap(new TreeMap()); 

6. Die synchronizedSet () -Methode

Als nächstes haben wir in dieser Überprüfung die synchronizedSet () -Methode. Wie der Name schon sagt, können wir mit minimalem Aufwand synchronisierte Sets erstellen .

Der Wrapper gibt eine thread-sichere Sammlung zurück, die durch den angegebenen Satz unterstützt wird :

Set syncSet = Collections.synchronizedSet(new HashSet()); 

7. Die synchronizedSortedSet () -Methode

Der letzte Synchronisations-Wrapper, den wir hier vorstellen, ist synchronizedSortedSet () .

Ähnlich wie bei anderen Wrapper-Implementierungen, die wir bisher überprüft haben, gibt die Methode eine thread-sichere Version des angegebenen SortedSet zurück :

SortedSet syncSortedSet = Collections.synchronizedSortedSet(new TreeSet()); 

8. Synchronisierte und gleichzeitige Sammlungen

Bis zu diesem Punkt haben wir uns die Synchronisations-Wrapper des Sammlungs-Frameworks genauer angesehen.

Now, let's focus on the differences between synchronized collections and concurrent collections, such as ConcurrentHashMap and BlockingQueue implementations.

8.1. Synchronized Collections

Synchronized collections achieve thread-safety through intrinsic locking, and the entire collections are locked. Intrinsic locking is implemented via synchronized blocks within the wrapped collection's methods.

As we might expect, synchronized collections assure data consistency/integrity in multi-threaded environments. However, they might come with a penalty in performance, as only one single thread can access the collection at a time (a.k.a. synchronized access).

For a detailed guide on how to use synchronized methods and blocks, please check our article on the topic.

8.2. Concurrent Collections

Concurrent collections (e.g. ConcurrentHashMap), achieve thread-safety by dividing their data into segments. In a ConcurrentHashMap, for example, different threads can acquire locks on each segment, so multiple threads can access the Map at the same time (a.k.a. concurrent access).

Concurrent collections are much more performant than synchronized collections, due to the inherent advantages of concurrent thread access.

So, the choice of what type of thread-safe collection to use depends on the requirements of each use case, and it should be evaluated accordingly.

9. Fazit

In diesem Artikel haben wir uns eingehend mit den Synchronisations-Wrappern befasst, die in der Collections- Klasse implementiert sind .

Darüber hinaus haben wir die Unterschiede zwischen synchronisierten und gleichzeitigen Sammlungen hervorgehoben und die Ansätze untersucht, die sie zur Erreichung der Thread-Sicherheit implementieren.

Wie üblich sind alle in diesem Artikel gezeigten Codebeispiele auf GitHub verfügbar.