So starten Sie einen Thread in Java

1. Einleitung

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

Dies ist sehr nützlich, insbesondere wenn lange oder wiederkehrende Vorgänge ausgeführt werden, die nicht im Hauptthread ausgeführt werden können , 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 den Lebenszyklus eines Threads in Java.

2. Die Grundlagen zum Ausführen eines Threads

Mithilfe des Thread- Frameworks können wir leicht eine Logik schreiben, die in einem parallelen Thread ausgeführt wird .

Versuchen wir ein einfaches Beispiel, indem wir die Thread- Klasse erweitern:

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(); } }

Wir sollten die start () -Methode für Threads im Status NEW aufrufen (das Äquivalent von nicht gestartet). Andernfalls löst Java eine Instanz der IllegalThreadStateException- Ausnahme aus.

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.

Dies ist natürlich weit entfernt von produktionsbereitem Code, bei dem es von entscheidender Bedeutung ist, Ressourcen richtig zu verwalten, um zu viel Kontextwechsel oder zu viel Speicherbedarf zu vermeiden.

Um produktionsbereit zu werden, müssen wir jetzt ein zusätzliches Boilerplate schreiben, um Folgendes zu erledigen:

  • die konsequente Erstellung neuer Threads
  • Die Anzahl der gleichzeitigen Live-Threads
  • 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. Das ExecutorService Framework

Der ExecutorService 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, wird ExecutorService mit einem einfachen Konstruktor und einigen Anpassungsoptionen geliefert, z. B. dem Warteschlangentyp, der minimalen und maximalen Anzahl von Threads und deren Namenskonvention.

Weitere Informationen zum ExecutorService finden Sie in unserem Handbuch zum Java ExecutorService.

4. Starten einer Aufgabe mit Executoren

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ückgibt, und submit , die eine Zukunft zurückgibt , die das Ergebnis der Berechnung kapselt.

Weitere Informationen zu Futures finden Sie in unserem Handbuch zu java.util.concurrent.Future.

5. Starten einer Aufgabe mit CompletableFutures

Um das Endergebnis von einem Future- Objekt abzurufen, können wir die im Objekt verfügbare get- Methode verwenden. Dies würde jedoch den übergeordneten Thread bis zum Ende der Berechnung blockieren.

Alternativ könnten wir den Block vermeiden, 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 Future- Konstrukt ein neues Framework eingeführt, um besser mit dem Ergebnis der Berechnung arbeiten zu können: CompletableFuture .

CompletableFuture implementiert CompletableStage , das eine große Auswahl an Methoden zum Anhängen von Rückrufen hinzufügt und alle Installationen vermeidet, 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 verwendet einen Lieferanten , der den Code enthält, den wir asynchron ausführen möchten - in unserem Fall den Lambda-Parameter.

Die Aufgabe wird jetzt implizit an ForkJoinPool.commonPool () gesendet , oder wir können den Executor, den wir bevorzugen, als zweiten Parameter angeben .

Um mehr über CompletableFuture zu erfahren , lesen Sie bitte unseren Leitfaden zu CompletableFuture.

6. Ausführen verzögerter oder periodischer Aufgaben

When working with complex web applications, we may need to run tasks at specific times, maybe regularly.

Java has few tools that can help us to run delayed or recurring operations:

  • java.util.Timer
  • java.util.concurrent.ScheduledThreadPoolExecutor

6.1. Timer

Timer is a facility to schedule tasks for future execution in a background thread.

Tasks may be scheduled for one-time execution, or for repeated execution at regular intervals.

Let's see what the code looks if we want to run a task after one second of delay:

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);

Now let's add a recurring schedule:

timer.scheduleAtFixedRate(repeatedTask, delay, period);

This time, the task will run after the delay specified and it'll be recurrent after the period of time passed.

For more information, please read our guide to Java Timer.

6.2. ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor has methods similar to the Timer class:

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

To end our example, we use scheduleAtFixedRate() for recurring tasks:

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

The code above will execute a task after an initial delay of 100 milliseconds, and after that, it'll execute the same task every 450 milliseconds.

If the processor can't finish processing the task in time before the next occurrence, the ScheduledExecutorService will wait until the current task is completed, before starting the next.

To avoid this waiting time, we can use scheduleWithFixedDelay(), which, as described by its name, guarantees a fixed length delay between iterations of the task.

Weitere Informationen zu ScheduledExecutorService finden Sie in unserem Handbuch zum Java ExecutorService.

6.3. Welches Tool ist besser?

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

So, wie wählen wir das richtige Werkzeug ?

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

Versuchen wir etwas tiefer unter die Haube zu tauchen.

Timer :

  • bietet keine Echtzeitgarantien: Es plant Aufgaben mit der Object.wait (long) -Methode
  • Es gibt einen einzelnen Hintergrund-Thread, sodass Aufgaben nacheinander ausgeführt werden und eine lang laufende Aufgabe andere verzögern kann
  • runtime exceptions thrown in a TimerTask would kill the only thread available, thus killing Timer

ScheduledThreadPoolExecutor:

  • can be configured with any number of threads
  • can take advantage of all available CPU cores
  • catches runtime exceptions and lets us handle them if we want to (by overriding afterExecute method from ThreadPoolExecutor)
  • cancels the task that threw the exception, while letting others continue to run
  • relies on the OS scheduling system to keep track of time zones, delays, solar time, etc.
  • provides collaborative API if we need coordination between multiple tasks, like waiting for the completion of all tasks submitted
  • provides better API for management of the thread life cycle

The choice now is obvious, right?

7. Difference Between Future and ScheduledFuture

In our code examples, we can observe that ScheduledThreadPoolExecutor returns a specific type of Future: ScheduledFuture.

ScheduledFuture extends both Future and Delayed interfaces, thus inheriting the additional method getDelay that returns the remaining delay associated with the current task. It's extended by RunnableScheduledFuture that adds a method to check if the task is periodic.

ScheduledThreadPoolExecutor implementiert alle diese Konstrukte über die innere Klasse ScheduledFutureTask und steuert damit den Task-Lebenszyklus.

8. Schlussfolgerungen

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

Dann haben wir uns eingehender mit den Unterschieden zwischen Timer und ScheduledThreadPoolExecutor befasst.

Der Quellcode für den Artikel ist auf GitHub verfügbar.