Project Loom: Java Virtual Threads sind geschlossen

Vor einigen Tagen brachte Ron Pressler in State of Loom einen Artikel zur Welt , den nur der faulste Javist nicht finden konnte. Der Artikel ist wirklich gut, es gibt viele interessante Metaphern, die ich jetzt ohne Bezugnahme auf die Quelle schamlos verwenden möchte.

Ich für meinen Teil habe mir versehentlich erlaubt, etwas Skepsis auszudrücken, aber wenn mit diesem Projekt Loom endlich wirklich gearbeitet werden kann. Buchstäblich eine Stunde später kam eine Antwort von Ron selbst - "und du versuchst es!" . Nun, ich musste es versuchen.

Was war für das Experiment erforderlich:


Zunächst habe ich mich entschlossen, mir anzusehen, wie ein klassisches Beispiel mit Streams aus der Dokumentation für Kotlin Coroutine aussehen würde . Das Beispiel ist in Kotlin geschrieben, aber das Umschreiben in Java ist nicht schwierig:

public class Main {
    public static void main(String[] args) {
        var c = new AtomicLong();
        for (var i = 0; i < 1_000_000; i++) {
            new Thread(() -> {
                c.incrementAndGet();
            }).start();
        }

        System.out.println(c.get());
    }
}

Wir beginnen und stellen sicher, dass das Beispiel nach wie vor hängt:

javac Main.java && java Main

Schreiben Sie nun das Beispiel mit den virtuellen Threads neu, die Project Loom uns zur Verfügung stellt:

for (var i = 0; i < 1_000_000; i++) {
    Thread.startVirtualThread(() -> {
        c.incrementAndGet();
    });
}

Das Ergebnis lässt nicht lange auf sich warten:

1000000

Wie lang?
, .
-, Gil Tene, — .
-, Project Loom, . , — .

Aber an sich hat es mich nicht allzu sehr beeindruckt. Am Ende wird mit Coroutinen in Kotlin das gleiche Ergebnis leicht erzielt.

Ron bemerkt in seinem Artikel richtig, dass Kotlin die delay () -Funktion einführen musste, die es der Coroutine ermöglicht, einzuschlafen, da Thread.sleep () die aktuelle Coroutine nicht in den Ruhezustand versetzt, sondern den aktuellen Scheduler-Thread, der normalerweise nicht viele ist, normalerweise nach Menge Zentralprozessor

Was ist mit Project Loom?

for (var i = 0; i < 1_000_000; i++) {
  Thread.startVirtualThread(() -> {
    c.incrementAndGet();
    try {
        Thread.sleep(1_000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
  });
}

Ergebnis:

- 400K

Das ist aber schon interessant! Mit Project Loom kann der Aufruf von Thread.sleep () unterscheiden, ob es sich um einen regulären Thread oder einen virtuellen Thread handelt, und es funktioniert anders.

Das an sich ist sehr cool. Aber lassen Sie uns etwas tiefer graben:

var threads = new ArrayList<Thread>();
var cores = 10;
for (var i = 0; i < cores; i++) {
    var t = Thread.startVirtualThread(() -> {
        var bestUUID = "";
        for (var j = 0; j < 1_000_000; j++) {
            var currentUUID = UUID.randomUUID().toString();
            if (currentUUID.compareTo(bestUUID) > 0) {
                bestUUID = currentUUID;
            }
        }
        System.out.println("Best slow UUID is " + bestUUID);
    });
    threads.add(t);
}

for (var i = 0; i < cores; i++) {
    var t = Thread.startVirtualThread(() -> {
        var bestUUID = UUID.randomUUID().toString();
        System.out.println("Best fast UUID is " + bestUUID);
    });
    threads.add(t);
}

for (Thread t : threads) {
    t.join();
}

Hier führen wir 10 langsame und 10 schnelle Aufgaben aus. Schnelle Aufgaben sind millionenfach schneller als langsame, daher wäre es logisch anzunehmen, dass sie früher erledigt werden.

Aber es war nicht da:

Best slow UUID is fffffde4-8c70-4ce6-97af-6a1779c206e1
Best slow UUID is ffffe33b-f884-4206-8e00-75bd78f6d3bd
Best slow UUID is fffffeb8-e972-4d2e-a1f8-6ff8aa640b70
Best fast UUID is e13a226a-d335-4d4d-81f5-55ddde69e554
Best fast UUID is ec99ed73-23b8-4ab7-b2ff-7942442a13a9
Best fast UUID is c0cbc46d-4a50-433c-95e7-84876a338212
Best fast UUID is c7672507-351f-4968-8cd2-2f74c754485c
Best fast UUID is d5ae642c-51ce-4b47-95db-abb6965d21c2
Best fast UUID is f2f942e3-f475-42b9-8f38-93d89f978578
Best fast UUID is 469691ee-da9c-4886-b26e-dd009c8753b8
Best fast UUID is 0ceb9554-a7e1-4e37-b477-064c1362c76e
Best fast UUID is 1924119e-1b30-4be9-8093-d5302b0eec5f
Best fast UUID is 94fe1afc-60aa-43ce-a294-f70f3011a424
Best slow UUID is fffffc24-28c5-49ac-8e30-091f1f9b2caf
Best slow UUID is fffff303-8ec1-4767-8643-44051b8276ca
Best slow UUID is ffffefcb-614f-48e0-827d-5e7d4dea1467
Best slow UUID is fffffed1-4348-456c-bc1d-b83e37d953df
Best slow UUID is fffff6d6-6250-4dfd-8d8d-10425640cc5a
Best slow UUID is ffffef57-c3c3-46f5-8ac0-6fad83f9d4d6
Best slow UUID is fffff79f-63a6-4cfa-9381-ee8959a8323d


Intuition funktioniert nur, solange die Anzahl der langsamen Aufgaben geringer ist als die Anzahl der Kerne Ihrer CPU.

Der Grund ist einfach - im Moment verwendet Project Loom den üblichen ForkJoinPool. Trotz der Tatsache, dass die Dokumentation und die Idee darauf hinweisen, dass virtuelle Streams „präemtiv“ sind, verhalten sie sich derzeit „kooperativ“. Eigentlich wie die Coroutinen in Kotlin.

Es sollte beachtet werden, dass Ron in dem oben genannten Artikel erwähnt, dass er auch über erzwungenes Präemtionsverhalten nachdenkt, wie in normalen Threads. Bisher wurde dies jedoch nicht implementiert, da nicht ganz klar ist, wie nützlich ein solches Verhalten ist, wenn es Zehntausende von Threads geben kann. In Go 1.14 wurde jedoch leise eine erzwungene Prävention eingeführt .

Ein Funktionsaufruf führt im Gegensatz zu Go nicht zu einem Kontextwechsel. Suspend wurde wie in Kotlin ebenfalls nicht ausgeliefert. Sie können dies jedoch mit Thread.yield () oder durch Aufrufen einer beliebigen Java-E / A-Funktion tun: System.out.println ("").

Die Berechnung ist einfach - die meisten realen Programme verwenden häufig blockierende E / A. Und es ist seine Verwendung von Project Loom, die in erster Linie zu lösen versucht.

Einige Schlussfolgerungen:


Ich muss zugeben, dass mich Project Loom trotz meiner anfänglichen Skepsis positiv beeindruckt hat. Für normale Java-Benutzer verspricht es leichtes Multithreading, ohne dass mithilfe einer Bibliothek oder eines Frameworks in eine andere Sprache gewechselt werden muss. Was sich schon gut anhört.

Aber die Hauptrevolution, wie ich erwarte, wird dieses Projekt unter Bibliotheksentwicklern durchgeführt, die das Parallelitätsproblem immer wieder und immer wieder lösen müssen. Mit der Verteilung von JDK15 kann dieses Problem nun auf die JVM übertragen werden, da sie zur Übertragung der Speicher- (GC) und Codeoptimierung (JIT) an die JVM verwendet wurden.

Link zu meinem Originalartikel, wenn Sie lieber auf Englisch lesen möchten.

All Articles