© Excellent backgrounds/Shutterstock.com
Kolumne: Java-Trickkiste

Kolumne: Java-Trickkiste


Moderne Systeme arbeiten eigentlich immer mit mehreren Threads. Und manchmal muss man sich trotz Frameworkunterstützung selbst darum kümmern, wie mehrere Threads auf dieselben Daten zugreifen.

In einem meiner Projekte wollten wir Kennzahlen sammeln und aggregiert zur Auswertung bereitstellen. Stark vereinfacht gab es eine Klasse, bei der Anwendungscode Messwerte registrieren konnte und die Klasse sich intern die Zahl der Messwerte sowie ihre Summe gemerkt hat. Listing 1 zeigt eine einfache Implementierung.

Listing 1

class CountAndSumSync { private int count = 1; private int sum; public CountAndSumSync(int initialValue) { sum = initialValue; } public synchronized void add(int value) { count += 1; sum += value; } public synchronized double getAverage() { if(count == 0) return 0; return 1.0 * sum / count; } }

Viele verschiedene Threads greifen gleichzeitig darauf zu, und deshalb sind alle Methoden synchronized. Es gibt eine ganze Reihe dieser Datentöpfe, die in einer Map abgelegt sind (Listing 2). Verschiedene Threads können parallel neue Datentöpfe anlegen, weshalb eine ConcurrentHashMap zum Einsatz kommt.

Listing 2

final Map<Integer, CountAndSumSync> mapSync = new ConcurrentHashMap<>(); void updateSynchronized (Integer key, int value) { if(mapSync.containsKey(key)) { mapSync.get(key).add(value); } else { mapSync.put(key, new CountAndSumSync(value)); } }

Beim Hinzufügen eines neuen Werts gibt es zwei Fälle. Entweder ist für den Schlüssel schon ein Eintrag hinterlegt, dann wird der Wert dort hinzugefügt. Oder es ist der erste Wert für diesen Schlüssel, dann wird ein neues CountAndSumSync-Objekt erzeugt und in die Map gelegt.

Wenn zwei Threads „gleichzeitig“ einen ersten Wert für einen neuen Key anlegen, wird eventuell einer von ihnen verworfen. Diese Unschärfe ist in unserem Beispiel aber akzeptabel.

Performancemessungen

Das Erfassen der Kennzahlen soll die eigentliche Anwendung natürlich möglichst wenig bremsen. Messen wir also, wie viel Zeit unsere Implementierung frisst. Dazu lassen wir mehrere Threads parallel Werte ändern und auslesen (Listing 3). Das Auslesen ist dabei wichtig, weil Lese- und Schreibzugriffe miteinander kollidieren können.

Listing 3

final int NUM_THREADS = 10; final int NUM_KEYS = 10; final CountDownLatch latch = new CountDownLatch(NUM_THREADS); for(int i=0; i<NUM_THREADS; i++) { new Thread(latch) { final Random rand = new Random(); public void run() { for(int i=0; i<1_000_000; i++) { try { for(int j=0; j<10; j++) { mapSync.get(ran...

Neugierig geworden?

Angebote für Teams

Für Firmen haben wir individuelle Teamlizenzen. Wir erstellen Ihnen gerne ein passendes Angebot.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang