So starten Sie einen Thread in Java

So starten Sie einen Thread in Java

1. Einführung

In diesem Tutorial werden wir verschiedene Möglichkeiten untersuchen, um einen Thread zu starten und parallele Aufgaben auszuführen.

This is very useful, in particular when dealing with long or recurring operations that can’t run on the main thread oder wenn die UI-Interaktion nicht angehalten werden kann, während auf die Ergebnisse des Vorgangs gewartet wird.

Um mehr über die Details von Threads zu erfahren, lesen Sie auf jeden Fall unser Tutorial über dieLife Cycle of a Thread in Java.

2. Grundlagen zum Ausführen eines Threads

Mit dem Framework vonThreadkönnen wir leicht eine Logik schreiben, die in einem parallelen Thread ausgeführt wird.

Versuchen wir ein einfaches Beispiel, indem wir die KlasseThreaderweitern:

public class NewThread extends Thread {
    public void run() {
        long startTime = System.currentTimeMillis();
        int i = 0;
        while (true) {
            System.out.println(this.getName() + ": New Thread is running..." + i++);
            try {
                //Wait for one sec so it doesn't print too fast
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ...
        }
    }
}

Und jetzt schreiben wir eine zweite Klasse, um unseren Thread zu initialisieren und zu starten:

public class SingleThreadExample {
    public static void main(String[] args) {
        NewThread t = new NewThread();
        t.start();
    }
}

Nehmen wir nun an, wir müssen mehrere Threads starten:

public class MultipleThreadsExample {
    public static void main(String[] args) {
        NewThread t1 = new NewThread();
        t1.setName("MyThread-1");
        NewThread t2 = new NewThread();
        t2.setName("MyThread-2");
        t1.start();
        t2.start();
    }
}

Unser Code sieht immer noch recht einfach aus und ist den Beispielen, die wir online finden, sehr ähnlich.

Natürlichthis is far from production-ready code, where it’s of critical importance to manage resources in the correct way, to avoid too much context switching or too much memory usage.

So, to get production-ready we now need to write additional boilerplate zu behandeln:

  • die konsequente Erstellung neuer Threads

  • Die Anzahl der gleichzeitig laufenden Threads

  • Die Freigabe der Threads: Sehr wichtig für Daemon-Threads, um Lecks zu vermeiden

Wenn wir wollen, können wir unseren eigenen Code für all diese und noch einige weitere Szenarien schreiben, aber warum sollten wir das Rad neu erfinden?

3. DasExecutorService Framework

DasExecutorService implementiert das Thread-Pool-Entwurfsmuster (auch als repliziertes Worker- oder Worker-Crew-Modell bezeichnet) und kümmert sich um das oben erwähnte Thread-Management. Außerdem werden einige sehr nützliche Funktionen wie die Wiederverwendbarkeit von Threads und Aufgabenwarteschlangen hinzugefügt.

Insbesondere die Wiederverwendbarkeit von Threads ist sehr wichtig: In einer umfangreichen Anwendung verursacht das Zuweisen und Freigeben vieler Thread-Objekte einen erheblichen Speicherverwaltungsaufwand.

Mit Worker-Threads minimieren wir den durch die Thread-Erstellung verursachten Overhead.

Um die Poolkonfiguration zu vereinfachen, enthältExecutorService einen einfachen Konstruktor und einige Anpassungsoptionen, z. B. den Warteschlangentyp, die minimale und maximale Anzahl von Threads und deren Namenskonvention.

Weitere Informationen zuExecutorService,finden Sie in unserenGuide to the Java ExecutorService.

4. Starten einer Aufgabe mit Executors

Dank dieses leistungsstarken Frameworks können wir unsere Denkweise vom Starten von Threads zum Senden von Aufgaben ändern.

Schauen wir uns an, wie wir eine asynchrone Aufgabe an unseren Executor senden können:

ExecutorService executor = Executors.newFixedThreadPool(10);
...
executor.submit(() -> {
    new Task();
});

Es gibt zwei Methoden, die wir verwenden können:execute, die nichts zurückgeben, undsubmit, dieFuture zurückgeben, die das Ergebnis der Berechnung kapseln.

Weitere Informationen zuFutures, finden Sie in unserenGuide to java.util.concurrent.Future.

5. Starten einer Aufgabe mitCompletableFutures

Um das Endergebnis von einemFuture-Objekt abzurufen, können wir die im Objekt verfügbareget-Methode verwenden, dies würde jedoch den übergeordneten Thread bis zum Ende der Berechnung blockieren.

Alternativ könnten wir den Block umgehen, indem wir unserer Aufgabe mehr Logik hinzufügen, aber wir müssen die Komplexität unseres Codes erhöhen.

Java 1.8 hat zusätzlich zum KonstruktFutureein neues Framework eingeführt, um besser mit dem Ergebnis der Berechnung arbeiten zu können:CompletableFuture.

CompletableFuture implementiertCompletableStage, wodurch eine große Auswahl an Methoden zum Anhängen von Rückrufen hinzugefügt wird und alle Installationen vermieden werden, die erforderlich sind, um Operationen am Ergebnis auszuführen, nachdem es fertig ist.

Die Implementierung zum Einreichen einer Aufgabe ist viel einfacher:

CompletableFuture.supplyAsync(() -> "Hello");

supplyAsync benötigtSupplier, die den Code enthalten, den wir asynchron ausführen möchten - in unserem Fall den Lambda-Parameter.

Die Aufgabe wird jetzt implizit anForkJoinPool.commonPool() übergeben, oder wir können dieExecutor, die wir bevorzugen, als zweiten Parameter angeben.

Um mehr überCompletableFuture, zu erfahren, lesen Sie bitte unsereGuide To CompletableFuture.

6. Ausführen von verzögerten oder periodischen Aufgaben

Bei der Arbeit mit komplexen Webanwendungen müssen wir möglicherweise Aufgaben zu bestimmten Zeiten ausführen, möglicherweise regelmäßig.

Java bietet nur wenige Tools, mit denen wir verzögerte oder wiederkehrende Vorgänge ausführen können:

  • java.util.Timer

  • java.util.concurrent.ScheduledThreadPoolExecutor

6.1. Timer

Timer ist eine Funktion zum Planen von Aufgaben für die zukünftige Ausführung in einem Hintergrundthread.

Aufgaben können für die einmalige Ausführung oder für die wiederholte Ausführung in regelmäßigen Abständen geplant werden.

Mal sehen, wie der Code aussieht, wenn wir eine Aufgabe nach einer Sekunde Verzögerung ausführen möchten:

TimerTask task = new TimerTask() {
    public void run() {
        System.out.println("Task performed on: " + new Date() + "n"
          + "Thread's name: " + Thread.currentThread().getName());
    }
};
Timer timer = new Timer("Timer");
long delay = 1000L;
timer.schedule(task, delay);

Fügen wir nun einen wiederkehrenden Zeitplan hinzu:

timer.scheduleAtFixedRate(repeatedTask, delay, period);

Dieses Mal wird die Aufgabe nach der angegebenen Verzögerung ausgeführt und nach Ablauf der abgelaufenen Zeit wiederholt.

Weitere Informationen finden Sie in unserer Anleitung zuJava Timer.

6.2. ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor hat ähnliche Methoden wie die KlasseTimer:

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
ScheduledFuture resultFuture
  = executorService.schedule(callableTask, 1, TimeUnit.SECONDS);


Um unser Beispiel zu beenden, verwenden wirscheduleAtFixedRate() für wiederkehrende Aufgaben:

ScheduledFuture resultFuture
 = executorService.scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);


Der obige Code führt eine Aufgabe nach einer anfänglichen Verzögerung von 100 Millisekunden aus. Danach führt er alle 450 Millisekunden dieselbe Aufgabe aus.

Wenn der Prozessor die Verarbeitung der Aufgabe nicht rechtzeitig vor dem nächsten Auftreten beenden kann, wartenScheduledExecutorService, bis die aktuelle Aufgabe abgeschlossen ist, bevor sie mit der nächsten beginnen.

Um diese Wartezeit zu vermeiden, können wirscheduleWithFixedDelay() verwenden, was, wie durch seinen Namen beschrieben, eine Verzögerung mit fester Länge zwischen den Iterationen der Aufgabe garantiert.

Weitere Informationen zuScheduledExecutorService, finden Sie in unserenGuide to the Java ExecutorService.

6.3. Welches Tool ist besser?

Wenn wir die obigen Beispiele ausführen, sieht das Ergebnis der Berechnung gleich aus.

Alsohow do we choose the right tool?

Wenn ein Framework mehrere Auswahlmöglichkeiten bietet, ist es wichtig, die zugrunde liegende Technologie zu verstehen, um eine fundierte Entscheidung treffen zu können.

Versuchen wir, etwas tiefer unter die Haube zu tauchen.

Timer:

  • bietet keine Echtzeitgarantien: Es plant Aufgaben mit derObject.wait(long) -Smethod

  • Es gibt einen einzelnen Hintergrund-Thread, sodass Aufgaben nacheinander ausgeführt werden und eine lang laufende Aufgabe andere verzögern kann

  • Laufzeitausnahmen, die inTimerTask ausgelöst werden, würden den einzigen verfügbaren Thread beenden und somitTimer beenden

ScheduledThreadPoolExecutor:

  • kann mit einer beliebigen Anzahl von Threads konfiguriert werden

  • kann alle verfügbaren CPU-Kerne nutzen

  • fängt Laufzeitausnahmen ab und lässt sie behandeln, wenn wir wollen (indem wir die MethodeafterExecutevonThreadPoolExecutor überschreiben)

  • Bricht den Task ab, der die Ausnahme ausgelöst hat, während andere weiterhin ausgeführt werden

  • stützt sich auf das OS-Planungssystem, um Zeitzonen, Verzögerungen, Sonnenzeiten usw. zu verfolgen.

  • Bietet eine API für die Zusammenarbeit, wenn die Koordination zwischen mehreren Aufgaben erforderlich ist, z. B. das Warten auf den Abschluss aller übermittelten Aufgaben

  • Bietet eine bessere API für die Verwaltung des Thread-Lebenszyklus

Die Wahl liegt jetzt auf der Hand, oder?

7. Differenz zwischenFuture undScheduledFuture

In unseren Codebeispielen können wir beobachten, dassScheduledThreadPoolExecutor einen bestimmten Typ vonFuture zurückgibt:ScheduledFuture.

ScheduledFuture erweitert die SchnittstellenFuture undDelayed und erbt so die zusätzliche MethodegetDelay, die die verbleibende Verzögerung zurückgibt, die der aktuellen Aufgabe zugeordnet ist. Es wird umRunnableScheduledFuture erweitert, wodurch eine Methode hinzugefügt wird, mit der überprüft wird, ob die Aufgabe periodisch ist.

ScheduledThreadPoolExecutor implementiert alle diese Konstrukte durch die innere KlasseScheduledFutureTask und verwendet sie zur Steuerung des Task-Lebenszyklus.

8. Schlussfolgerungen

In diesem Lernprogramm haben wir mit den verschiedenen verfügbaren Frameworks experimentiert, um Threads zu starten und Aufgaben parallel auszuführen.

Dann gingen wir tiefer in die Unterschiede zwischenTimer undScheduledThreadPoolExecutor. ein

Der Quellcode für den Artikel istover on GitHub verfügbar.