Parallele Programmierung (3IB)
StructuredTaskScopeJava-Threads waren bisher immer “Plattform-Threads”.
Der “alte” Weg:
Ein gängiges Muster in Server-Anwendungen:
Problem: Was passiert, wenn 10.000 Clients gleichzeitig eine Anfrage stellen?
OutOfMemoryError).Seit Java 21 sind Virtuelle Threads (Teil von Project Loom) standardmäßig verfügbar.
Tipp
ideal für…
Wenn ein virtueller Thread auf eine I/O-Operation wartet…
Das ist der Game-Changer: Warten blockiert nicht mehr den wertvollen OS-Thread!
Hinweis: Viele virtuelle Threads teilen sich wenige Carrier-Threads (“M:N-Scheduling”) – blockierende I/O parkt den virtuellen Thread, nicht den Carrier-Thread.
ExecutorService für virtuelle ThreadsnewVirtualThreadPerTaskExecutor() für ExecutorServicecommonPool:Tipp
Der empfohlene Weg für die meisten Anwendungen.
ExecutorServiceSimulation von vielen kurzen I/O-lastigen Aufgaben:
void main() {
try (var executor = Executors newCachedThreadpool()) {
for (int i = 0; i <= 10000; i++) {
final int fi = i;
executor.submit(() -> {
IO.println(fi + ": " + Thread.currentThread());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}
}void main() {
try (var executor = Executors.newThreadPerTaskExecutor(Executors.defaultThreadFactory())) {
for (int i = 0; i <= 10000; i++) {
final int fi = i;
executor.submit(() -> {
IO.println(fi + ": " + Thread.currentThread());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}
}void main() {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i <= 10000; i++) {
final int fi = i;
executor.submit(() -> {
IO.println(fi + ": " + Thread.currentThread());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}
}seit Java 21 startVirtualThread(Runnable):
Thread)RunnablecommonPoolTipp
für Einzeiler
startVirtualThread(Runnable)Thread-Objekt)
VirtualThread[#35]/runnable@ForkJoinPool-1-worker-1
seit Java 21 Thread.ofVirtual() und Thread.ofPlatform():
Thread.ofPlatform() für Plattform-Threads.
setName(String)).
setUncaughtExceptionHandler)
Runnable und gibt die Thread-Referenz zurück.
ThreadFactory für virtuelle Threads.
ThreadFactory für Plattform-Threads.
Runnable.
Runnable r = () -> IO.println(Thread.currentThread().getName());
1Thread t = Thread
.ofVirtual()
.name("worker")
.unstarted(r);
t.start();
Thread.ofVirtual().start(r);
Thread.ofPlatform().start(r);Ein virtueller Thread kann an seinen Carrier-Thread “gepinnt” werden.
Pinning bedeutet, dass der virtuelle Thread nicht vom Carrier-Thread entkoppelt werden kann, selbst wenn er blockiert.
Konsequenz: Der Carrier-Thread ist blockiert und kann keine anderen virtuellen Threads ausführen. Dies untergräbt den gesamten Vorteil von virtuellen Threads!
Warnung
Achtung: Pinning kann die Skalierbarkeit drastisch reduzieren! Das möchte man vermeinden!!
Die zwei häufigsten Ursachen für Pinning sind:
synchronized-Blöcke/-Methoden: Wenn ein virtueller Thread innerhalb eines synchronized-Blocks eine blockierende Operation ausführt (z.B. I/O, Thread.sleep, LockSupport.park), wird er an den Carrier gepinnt.ReentrantLockStatt synchronized sollte man java.util.concurrent.locks.ReentrantLock verwenden.
ReentrantLock ist “virtual-thread-aware” und führt nicht zu Pinning.
Tipp
Faustregel: Ersetze synchronized durch ReentrantLock in Code, der von virtuellen Threads ausgeführt wird.
synchronized-Blöcken; stattdessen
ReentrantLock (ggf. tryLock mit Timeout)ReadWriteLock/StampedLock für lese-lastige WorkloadsThread.startVirtualThread(...)Executors.newVirtualThreadPerTaskExecutor()StructuredTaskScope gezielt limitierenPreview Feature in Java 25
viele Änderungen zu vorigen Versionen, noch nicht stabil – zukünftig noch Änderungen!!
java --enable-preview Main.java
Traditionelle Concurrency ist oft “fire-and-forget”.
Structured Concurrency behandelt nebenläufige Aufgaben als eine einzige Arbeitseinheit.
StructuredTaskScopeDas Kernkonzept ist der StructuredTaskScope: ermöglicht Strukturierung und Kontrolle der Lebenszeit für nebenläufige Aufgaben
fork) werden, müssen beendet sein, bevor der Hauptthread den Scope-Block verlässt.Erreicht wird dies durch unterschiedliche Joiner-Strategien.
Joiner: Shutdown on FailureWarten auf alle erfolgreichen Tasks: Liefert einen Stream der Ergebnisse
allSuccessfulOrThrow: Bei einem Fehler wird der Scope abgebrochen und alle Tasks beendet. Ansonsten wird gewartet, bis alle Tasks erfolgreich sind.awaitAllSuccessfulOrThrow: wie allSuccessfulOrThrow, wenn Tasks Ergebnisse unterschiedlichen Typs haben.awaitAll: wie allSuccessfulOrThrow, wenn Tasks keinen Rückgabewert haben.Joiner: Shutdown on SuccessWarten auf den ersten erfolgreichen Task oder spezifischere Bedingung:
anySuccessfulResultOrThrow: Der Scope beendet sich, sobald eine Task erfolgreich abgeschlossen ist.allUntil(Predicate): Der Scope beendet sich, sobald die Bedingung Predicate erfüllt ist oder alle Tasks abgeschlossen sind.Las Vegas Algorithmus zur Primzahlerzeugung (z.B. für RSA-Schlüsselerzeugung)
structuredPrime.java
import java.util.concurrent.StructuredTaskScope.Joiner;
BigInteger parallelPrime(int bitLength) throws InterruptedException {
try (var scope = StructuredTaskScope.open(Joiner.<BigInteger>anySuccessfulResultOrThrow())) {
for (var i = 0; i < ForkJoinPool.commonPool().getParallelism(); i++) {
scope.fork(() -> BigInteger.probablePrime(bitLength, ThreadLocalRandom.current()));
}
return scope.join();
}
}
void main() throws InterruptedException {
IO.println(parallelPrime(4096));
}Kompilieren und Ausführen mit Preview-Features
Thread.ofVirtual(), Executors.newVirtualThreadPerTaskExecutor()) macht die Verwendung einfach.synchronized in Code, der von virtuellen Threads ausgeführt wird, und nutze stattdessen ReentrantLock.StructuredTaskScope) bringt Ordnung in nebenläufige Abläufe, verbessert die Fehlerbehandlung und macht den Code verständlicher.