Dr. Heinz Kabutz Selbstständig

In Phaser lässt sich ein Baum einrichten, wodurch wir weniger Konflikte erhalten.

Java 7 brachte uns eine neue Klasse namens Phaser, mit der wir Aktivitäten zwischen Threads koordinieren können. Sie ersetzt sowohl CountDownLatch als auch CyclicBarrier, die zwar einfacher zu verstehen, aber schwieriger zu bedienen sind.

Ich spreche seit vielen Jahren in meinem Extreme-Java-Concurrency-Performance-Kurs über Phaser und habe noch keinen Teilnehmer gefunden, der mir sagt: „Oh ja, das ist eine tolle Klasse, wir benutzen sie ständig!“ Die Teilnehmer haben in der Regel schon von CountDownLatch und vielleicht CyclicBarrier gehört, aber selten von Phaser. Wie kann das sein, wenn Phaser seit Java 7 existiert und die Synchronisierung zwischen Threads so viel einfacher macht als andere ähnliche Konstrukte?

CountDownLatch ist einfach zu verstehen, aber schwierig zu handhaben. Phaser hingegen ist schwer zu verstehen, aber einfach anzuwenden. Kürzlich unterrichtete ich eine Gruppe cleverer Programmierer in Athen. Eine der vielen klugen Fragen lautete: „Wie können wir eine Reihe von Aufgaben koordinieren, die alle unterschiedlich viel Zeit in Anspruch nehmen?“ Meine erste, spontane Antwort lautete, CompletionStage zu benutzen. Aber je intensiver wir uns mit dem Problem auseinandersetzten, desto besser schien Phaser zu passen. Im Kurs habe ich zuerst mittels Phaser codiert. Jemand fragte dann, ob dasselbe mit CountDownLatch möglich wäre. Also haben wir auch das kodiert. Hier werde ich es umgekehrt machen. Wir beginnen mit CountDownLatch und refaktorieren dann, um stattdessen Phaser zu verwenden.

Erst mal aufteilen

Wir werden fünf Batches ausführen. Jedes Batch besteht aus drei Aufgaben, die zwischen 500 Millisekunden und 3 Sekunden dauern. Aufgaben innerhalb eines Batches sollten alle gleichzeitig beginnen. Um den Code leichter lesbar zu machen, definieren wir eine gemeinsame Oberklasse LockStepExample (Listing 1).

import java.util.concurrent.*;

public abstract class LockStepExample {
  protected final static int TASKS_PER_BATCH = 3;
  protected final static int BATCHES = 5;

  protected final void doTask(int batch) {
    System.out.printf("Task start %d%n", batch);
    int ms = ThreadLocalRandom.current().nextInt(500, 3000);
    try {
      Thread.sleep(ms);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
    System.out.printf("Task in batch %d took %dms%n", batch, ms);
  }
}

Als Nächstes erweitern wir den Code mit unserem LockStepCountDownLatch. Da das CountDownLatch nicht zurückgesetzt werden kann, müssen wir für jeden Batch ein neues latch erstellen. latch verfügt über ein ziemlich veraltetes Interrupt Handling. Es gibt keine Möglichkeit, den Interrupt so lange zu speichern, bis wir fertig sind, wie wir es mit Semaphore.acquireUninterruptibly() oder Lock.lock() tun können.

Den vollständigen Artikel lesen Sie in der Ausgabe:

Java Magazin 9.18 - "Jakarta EE"

Alle Infos zum Heft
579851297„CountDownLatch“ vs „CyclicBarrier“ vs „Phaser“
X
- Gib Deinen Standort ein -
- or -