Unterschied zwischen Thread und virtuellem Thread in Java

1. Einleitung

In diesem Tutorial zeigen wir den Unterschied zwischen herkömmlichen Threads in Java und den in Project Loom eingeführten virtuellen Threads.

Als Nächstes werden einige Anwendungsfälle für virtuelle Threads und die vom Projekt eingeführten APIs erläutert.

Bevor wir beginnen, müssen wir beachten, dass sich dieses Projekt in der aktiven Entwicklung befindet. Wir werden unsere Beispiele auf einer VM mit frühem Zugriff ausführen: openjdk-15-loom + 4-55_windows-x64_bin.

Neuere Versionen der Builds können aktuelle APIs ändern und beschädigen. Davon abgesehen gab es bereits eine wesentliche Änderung in der API, da die zuvor verwendete java.lang.Fiber- Klasse entfernt und durch die neue java.lang.VirtualThread- Klasse ersetzt wurde.

2. Übersicht über Thread und virtuellen Thread auf hoher Ebene

Auf hoher Ebene wird ein Thread vom Betriebssystem verwaltet und geplant, während ein virtueller Thread von einer virtuellen Maschine verwaltet und geplant wird . Um einen neuen Kernel-Thread zu erstellen, müssen wir einen Systemaufruf ausführen, und das ist eine kostspielige Operation .

Aus diesem Grund verwenden wir Thread-Pools, anstatt Threads nach Bedarf neu zuzuweisen und freizugeben. Wenn wir unsere Anwendung aufgrund des Kontextwechsels und ihres Speicherbedarfs durch Hinzufügen weiterer Threads skalieren möchten, können die Kosten für die Wartung dieser Threads erheblich sein und sich auf die Verarbeitungszeit auswirken.

In der Regel möchten wir diese Threads dann nicht blockieren. Dies führt dazu, dass nicht blockierende E / A-APIs und asynchrone APIs verwendet werden, wodurch unser Code möglicherweise unübersichtlich wird.

Im Gegenteil, virtuelle Threads werden von der JVM verwaltet . Daher erfordert ihre Zuordnung keinen Systemaufruf und sie sind frei von der Kontextumschaltung des Betriebssystems . Darüber hinaus werden virtuelle Threads auf dem Träger-Thread ausgeführt, bei dem es sich um den eigentlichen Kernel-Thread handelt, der unter der Haube verwendet wird. Da wir nicht im Kontextwechsel des Systems sind, können wir daher viel mehr solcher virtuellen Threads erzeugen.

Als nächstes ist eine Schlüsseleigenschaft von virtuellen Threads, dass sie unseren Carrier-Thread nicht blockieren. Damit wird das Blockieren eines virtuellen Threads zu einem viel billigeren Vorgang, da die JVM einen anderen virtuellen Thread plant und den Carrier-Thread nicht blockiert.

Letztendlich müssten wir nicht nach NIO- oder Async-APIs greifen. Dies sollte zu besser lesbarem Code führen, der leichter zu verstehen und zu debuggen ist. Trotzdem kann die Fortsetzung möglicherweise einen Träger-Thread blockieren - insbesondere, wenn ein Thread eine native Methode aufruft und von dort aus Blockierungsvorgänge ausführt.

3. Neue Thread Builder-API

In Loom haben wir die neue Builder-API in der Thread- Klasse zusammen mit mehreren Factory-Methoden erhalten. Mal sehen, wie wir Standard- und virtuelle Fabriken erstellen und für unsere Thread-Ausführung verwenden können:

Runnable printThread = () -> System.out.println(Thread.currentThread()); ThreadFactory virtualThreadFactory = Thread.builder().virtual().factory(); ThreadFactory kernelThreadFactory = Thread.builder().factory(); Thread virtualThread = virtualThreadFactory.newThread(printThread); Thread kernelThread = kernelThreadFactory.newThread(printThread); virtualThread.start(); kernelThread.start();

Hier ist die Ausgabe des obigen Laufs:

Thread[Thread-0,5,main] VirtualThread[,ForkJoinPool-1-worker-3,CarrierThreads]

Hier ist der erste Eintrag die Standard- toString- Ausgabe des Kernel-Threads.

In der Ausgabe sehen wir nun, dass der virtuelle Thread keinen Namen hat und auf einem Arbeitsthread des Fork-Join-Pools aus der CarrierThreads-Threadgruppe ausgeführt wird.

Wie wir sehen können, ist die API unabhängig von der zugrunde liegenden Implementierung dieselbe, und dies impliziert, dass wir vorhandenen Code auf den virtuellen Threads problemlos ausführen können .

Außerdem müssen wir keine neue API lernen, um sie nutzen zu können.

4. Zusammensetzung des virtuellen Threads

Es ist eine Fortsetzung und ein Scheduler , die zusammen einen virtuellen Thread bilden. Unser Scheduler im Benutzermodus kann nun eine beliebige Implementierung der Executor- Schnittstelle sein. Das obige Beispiel hat uns gezeigt, dass wir standardmäßig auf dem ForkJoinPool ausgeführt werden .

Ähnlich wie bei einem Kernel-Thread, der auf der CPU ausgeführt, dann geparkt, neu geplant und dann wieder ausgeführt werden kann, ist eine Fortsetzung eine Ausführungseinheit, die gestartet, dann geparkt (nachgegeben), neu geplant und fortgesetzt werden kann Die Ausführung erfolgt auf die gleiche Weise, von wo aus sie aufgehört hat und dennoch von einer JVM verwaltet wird, anstatt sich auf ein Betriebssystem zu verlassen.

Beachten Sie, dass die Fortsetzung eine API auf niedriger Ebene ist und dass Programmierer APIs auf höherer Ebene wie die Builder-API verwenden sollten, um virtuelle Threads auszuführen.

Um zu zeigen, wie es unter der Haube funktioniert, führen wir jetzt unsere experimentelle Fortsetzung durch:

var scope = new ContinuationScope("C1"); var c = new Continuation(scope, () -> { System.out.println("Start C1"); Continuation.yield(scope); System.out.println("End C1"); }); while (!c.isDone()) { System.out.println("Start run()"); c.run(); System.out.println("End run()"); }

Hier ist die Ausgabe des obigen Laufs:

Start run() Start C1 End run() Start run() End C1 End run()

In diesem Beispiel haben wir unsere Fortsetzung ausgeführt und irgendwann beschlossen, die Verarbeitung zu stoppen. Sobald wir es erneut ausgeführt hatten, setzte sich unsere Fortsetzung dort fort, wo es aufgehört hatte. An der Ausgabe sehen wir, dass die run () -Methode zweimal aufgerufen wurde, die Fortsetzung jedoch einmal gestartet wurde und dann beim zweiten Lauf dort ausgeführt wurde, wo sie aufgehört hat.

Auf diese Weise sollen Blockierungsvorgänge von der JVM verarbeitet werden. Sobald ein Blockierungsvorgang stattfindet, gibt die Fortsetzung nach und der Trägerfaden wird nicht blockiert.

Was also passiert ist, ist, dass unser Hauptthread einen neuen Stapelrahmen auf seinem Aufrufstapel für die run () -Methode erstellt und mit der Ausführung fortgefahren hat. Nachdem die Fortsetzung nachgegeben hatte, speicherte die JVM den aktuellen Status ihrer Ausführung.

Als nächstes hat der Hauptthread seine Ausführung fortgesetzt, als ob die run () -Methode zurückgegeben und mit der while- Schleife fortgesetzt worden wäre . Nach dem zweiten Aufruf der Ausführungsmethode von Continuation stellte die JVM den Status des Hauptthreads bis zu dem Punkt wieder her, an dem die Fortsetzung nachgegeben und die Ausführung beendet hat.

5. Schlussfolgerung

In diesem Artikel haben wir den Unterschied zwischen dem Kernel-Thread und dem virtuellen Thread erörtert. Als Nächstes haben wir gezeigt, wie wir eine neue Thread Builder-API von Project Loom verwenden können, um die virtuellen Threads auszuführen.

Schließlich haben wir gezeigt, was eine Fortsetzung ist und wie sie unter der Haube funktioniert. Wir können den Status von Project Loom weiter untersuchen, indem wir die Early Access VM untersuchen. Alternativ können wir weitere der bereits standardisierten Java-Parallelitäts-APIs untersuchen.