Threading-Modelle in Java

1. Einleitung

In unseren Anwendungen müssen wir häufig in der Lage sein, mehrere Dinge gleichzeitig zu erledigen. Wir können dies auf verschiedene Arten erreichen, aber der Schlüssel dazu ist die Implementierung von Multitasking in irgendeiner Form.

Multitasking bedeutet, mehrere Aufgaben gleichzeitig auszuführen , wobei jede Aufgabe ihre Arbeit ausführt. Diese Aufgaben werden normalerweise alle zur gleichen Zeit ausgeführt, wobei derselbe Speicher gelesen und geschrieben wird und mit denselben Ressourcen interagiert wird, jedoch unterschiedliche Aufgaben ausgeführt werden.

2. Native Threads

Die Standardmethode zum Implementieren von Multitasking in Java ist die Verwendung von Threads . Threading wird normalerweise bis zum Betriebssystem unterstützt. Wir nennen Threads, die auf dieser Ebene funktionieren, "native Threads".

Das Betriebssystem verfügt über einige Threading-Funktionen, die für unsere Anwendungen häufig nicht verfügbar sind, einfach weil sie näher an der zugrunde liegenden Hardware liegen. Dies bedeutet, dass die Ausführung nativer Threads in der Regel effizienter ist. Diese Threads werden direkt den Ausführungsthreads auf der Computer-CPU zugeordnet - und das Betriebssystem verwaltet die Zuordnung von Threads zu CPU-Kernen.

Das Standard-Threading-Modell in Java, das alle JVM-Sprachen abdeckt, verwendet native Threads . Dies ist seit Java 1.2 der Fall und gilt unabhängig vom zugrunde liegenden System, auf dem die JVM ausgeführt wird.

Dies bedeutet, dass wir jedes Mal, wenn wir einen der Standard-Threading-Mechanismen in Java verwenden, native Threads verwenden. Dies umfasst java.lang.Thread , java.util.concurrent.Executor , java.util.concurrent.ExecutorService usw.

3. Grüne Fäden

In der Softwareentwicklung sind grüne Threads eine Alternative zu nativen Threads . Hier verwenden wir Threads, die jedoch nicht direkt Betriebssystem-Threads zugeordnet sind. Stattdessen verwaltet die zugrunde liegende Architektur die Threads selbst und verwaltet, wie diese Betriebssystem-Threads zugeordnet werden.

In der Regel werden dazu mehrere native Threads ausgeführt und die grünen Threads dann zur Ausführung diesen nativen Threads zugewiesen . Das System kann dann auswählen, welche grünen Threads zu einem bestimmten Zeitpunkt aktiv sind und auf welchen nativen Threads sie aktiv sind.

Das klingt sehr kompliziert und ist es auch. Aber es ist eine Komplikation, um die wir uns im Allgemeinen nicht kümmern müssen. Die zugrunde liegende Architektur kümmert sich um all dies, und wir können es so verwenden, als wäre es ein natives Threading-Modell.

Warum sollten wir das tun? Native Threads sind sehr effizient auszuführen, verursachen jedoch hohe Kosten beim Starten und Stoppen. Grüne Fäden helfen, diese Kosten zu vermeiden und geben der Architektur viel mehr Flexibilität. Wenn wir relativ lange laufende Threads verwenden, sind native Threads sehr effizient. Bei sehr kurzlebigen Jobs können die Kosten für den Start den Nutzen der Nutzung überwiegen . In diesen Fällen können grüne Fäden effizienter werden.

Leider bietet Java keine integrierte Unterstützung für grüne Threads.

In sehr frühen Versionen wurden grüne Threads anstelle von nativen Threads als Standard-Threading-Modell verwendet. Dies hat sich in Java 1.2 geändert, und seitdem wurde es auf JVM-Ebene nicht mehr unterstützt.

Es ist auch schwierig, grüne Threads in Bibliotheken zu implementieren, da diese nur eine sehr geringe Unterstützung benötigen, um eine gute Leistung zu erzielen. Als solche werden häufig Fasern verwendet.

4. Fasern

Fasern sind eine alternative Form des Multithreading und ähneln grünen Fäden . In beiden Fällen verwenden wir keine nativen Threads, sondern die zugrunde liegenden Systemsteuerelemente, die jederzeit ausgeführt werden. Der große Unterschied zwischen grünen Fäden und Fasern besteht in der Kontrollebene und insbesondere darin, wer die Kontrolle hat.

Grüne Fäden sind eine Form des präventiven Multitasking. Dies bedeutet, dass die zugrunde liegende Architektur vollständig dafür verantwortlich ist, zu entscheiden, welche Threads zu einem bestimmten Zeitpunkt ausgeführt werden.

Dies bedeutet, dass alle üblichen Probleme beim Threading auftreten, bei denen wir nichts über die Reihenfolge wissen, in der unsere Threads ausgeführt werden oder welche gleichzeitig ausgeführt werden. Dies bedeutet auch, dass das zugrunde liegende System jederzeit in der Lage sein muss, unseren Code anzuhalten und neu zu starten, möglicherweise mitten in einer Methode oder sogar einer Anweisung.

Fasern sind stattdessen eine Form des kooperativen Multitasking, was bedeutet, dass ein laufender Thread so lange ausgeführt wird, bis er signalisiert, dass er einem anderen nachgeben kann . Es liegt in unserer Verantwortung, dass die Fasern zusammenarbeiten. Dies gibt uns die direkte Kontrolle darüber, wann die Fasern die Ausführung unterbrechen können, anstatt dass das System dies für uns entscheidet.

Dies bedeutet auch, dass wir unseren Code so schreiben müssen, dass dies möglich ist. Sonst funktioniert es nicht. Wenn unser Code keine Unterbrechungspunkte hat, können wir genauso gut überhaupt keine Fasern verwenden.

Java bietet derzeit keine integrierte Unterstützung für Fasern. Es gibt einige Bibliotheken, die dies in unsere Anwendungen einführen können, einschließlich, aber nicht beschränkt auf:

4.1. Quasar

Quasar ist eine Java-Bibliothek, die gut mit reinem Java und Kotlin funktioniert und eine alternative Version hat, die mit Clojure funktioniert.

Es funktioniert mit einem Java-Agenten, der neben der Anwendung ausgeführt werden muss. Dieser Agent ist dafür verantwortlich, die Fasern zu verwalten und sicherzustellen, dass sie ordnungsgemäß zusammenarbeiten. Die Verwendung eines Java-Agenten bedeutet, dass keine speziellen Erstellungsschritte erforderlich sind.

Quasar erfordert außerdem, dass Java 11 ordnungsgemäß funktioniert, sodass die Anwendungen, die es verwenden können, möglicherweise eingeschränkt werden. Ältere Versionen können unter Java 8 verwendet werden, diese werden jedoch nicht aktiv unterstützt.

4.2. Kelim

Kilim ist eine Java-Bibliothek, die Quasar sehr ähnliche Funktionen bietet, jedoch Bytecode-Weberei anstelle eines Java-Agenten verwendet . Dies bedeutet, dass es an mehreren Orten funktionieren kann, aber es macht den Erstellungsprozess komplizierter.

Kilim funktioniert mit Java 7 und neuer und funktioniert auch in Szenarien, in denen ein Java-Agent keine Option ist, ordnungsgemäß. Zum Beispiel, wenn bereits eine andere für die Instrumentierung oder Überwachung verwendet wird.

4.3. Projektwebstuhl

Project Loom ist ein Experiment des OpenJDK-Projekts zum Hinzufügen von Fasern zur JVM selbst und nicht als Add-On-Bibliothek . Dies gibt uns die Vorteile von Fasern gegenüber Fäden. Durch die direkte Implementierung in der JVM können Komplikationen vermieden werden, die durch Java-Agenten und das Weben von Bytecodes entstehen.

Es gibt keinen aktuellen Release-Zeitplan für Project Loom, aber wir können jetzt Binärdateien für den frühen Zugriff herunterladen, um zu sehen, wie die Dinge laufen. Da es jedoch noch sehr früh ist, müssen wir uns bei jedem Produktionscode darauf verlassen.

5. Co-Routinen

Co-Routinen sind eine Alternative zu Einfädeln und Fasern. Wir können uns Co-Routinen als Fasern ohne jegliche Planung vorstellen . Anstatt dass das zugrunde liegende System entscheidet, welche Aufgaben zu einem bestimmten Zeitpunkt ausgeführt werden, führt unser Code dies direkt aus.

Im Allgemeinen schreiben wir Co-Routinen so, dass sie an bestimmten Punkten ihres Flusses nachgeben. Diese können als Pausenpunkte in unserer Funktion angesehen werden, an denen sie nicht mehr funktionieren und möglicherweise ein Zwischenergebnis ausgeben. Wenn wir nachgeben, werden wir gestoppt, bis der aufrufende Code beschließt, uns aus irgendeinem Grund neu zu starten. Dies bedeutet, dass unser Aufrufcode die Planung steuert, wann dies ausgeführt wird.

Kotlin bietet native Unterstützung für Co-Routinen, die in seine Standardbibliothek integriert sind. Es gibt mehrere andere Java-Bibliotheken, mit denen wir sie bei Bedarf ebenfalls implementieren können.

6. Fazit

Wir haben in unserem Code verschiedene Alternativen für Multitasking gesehen, die von den traditionellen nativen Threads bis zu einigen sehr leichten Alternativen reichen. Probieren Sie sie aus, wenn eine Anwendung das nächste Mal Parallelität benötigt.