So beenden Sie die Ausführung nach einer bestimmten Zeit in Java

1. Übersicht

In diesem Artikel erfahren Sie, wie Sie eine Ausführung mit langer Laufzeit nach einer bestimmten Zeit beenden können. Wir werden die verschiedenen Lösungen für dieses Problem untersuchen. Außerdem werden wir einige ihrer Fallstricke behandeln.

2. Verwenden einer Schleife

Stellen Sie sich vor, wir verarbeiten eine Reihe von Artikeln in einer Schleife, z. B. einige Details der Produktartikel in einer E-Commerce-Anwendung, aber möglicherweise müssen nicht alle Artikel vervollständigt werden.

Tatsächlich möchten wir nur bis zu einer bestimmten Zeit verarbeiten, und danach möchten wir die Ausführung stoppen und anzeigen, was die Liste bis zu diesem Zeitpunkt verarbeitet hat.

Sehen wir uns ein kurzes Beispiel an:

long start = System.currentTimeMillis(); long end = start + 30*1000; while (System.currentTimeMillis() < end) { // Some expensive operation on the item. }

Hier wird die Schleife unterbrochen, wenn die Zeit das Limit von 30 Sekunden überschritten hat. Die obige Lösung enthält einige bemerkenswerte Punkte:

  • Geringe Genauigkeit: Die Schleife kann länger als das festgelegte Zeitlimit laufen . Dies hängt von der Zeit ab, die jede Iteration dauern kann. Wenn beispielsweise jede Iteration bis zu 7 Sekunden dauern kann, kann die Gesamtzeit bis zu 35 Sekunden betragen, was etwa 17% länger ist als das gewünschte Zeitlimit von 30 Sekunden
  • Blockieren: Eine solche Verarbeitung im Hauptthread ist möglicherweise keine gute Idee, da sie für lange Zeit blockiert wird . Stattdessen sollten diese Operationen vom Hauptthread entkoppelt werden

Im nächsten Abschnitt werden wir diskutieren, wie der Interrupt-basierte Ansatz diese Einschränkungen beseitigt.

3. Verwenden eines Interrupt-Mechanismus

Hier verwenden wir einen separaten Thread, um die lang laufenden Vorgänge auszuführen. Der Haupt-Thread sendet bei Zeitüberschreitung ein Interrupt-Signal an den Worker-Thread.

Wenn der Worker-Thread noch aktiv ist, fängt er das Signal ab und stoppt seine Ausführung. Wenn der Worker vor dem Timeout beendet wird, hat dies keine Auswirkungen auf den Worker-Thread.

Werfen wir einen Blick auf den Arbeitsthread:

class LongRunningTask implements Runnable { @Override public void run() { try { while (!Thread.interrupted()) { Thread.sleep(500); } } catch (InterruptedException e) { // log error } } }

Hier simuliert Thread.sleep einen lang laufenden Vorgang. Stattdessen könnte es eine andere Operation geben. Es ist wichtig, das Interrupt-Flag zu überprüfen, da nicht alle Vorgänge unterbrechbar sind . In diesen Fällen sollten wir das Flag manuell überprüfen.

Außerdem sollten wir dieses Flag in jeder Iteration überprüfen, um sicherzustellen, dass der Thread nicht mehr innerhalb der Verzögerung von höchstens einer Iteration ausgeführt wird.

Als nächstes werden drei verschiedene Mechanismen zum Senden des Interrupt-Signals behandelt.

3.1. Verwenden eines Timers

Alternativ können wir eine TimerTask erstellen, um den Worker-Thread bei Zeitüberschreitung zu unterbrechen:

class TimeOutTask extends TimerTask { private Thread t; private Timer timer; TimeOutTask(Thread t, Timer timer){ this.t = t; this.timer = timer; } public void run() { if (t != null && t.isAlive()) { t.interrupt(); timer.cancel(); } } }

Hier haben wir eine TimerTask definiert , die zum Zeitpunkt ihrer Erstellung einen Arbeitsthread verwendet . Es wird der Worker - Thread auf den Aufruf der Interrupt - run - Methode . Der Timer löst die TimerTask nach der angegebenen Verzögerung aus:

Thread t = new Thread(new LongRunningTask()); Timer timer = new Timer(); timer.schedule(new TimeOutTask(t, timer), 30*1000); t.start();

3.2. Verwenden der Methode Future # get

Wir können auch die get- Methode einer Zukunft verwenden, anstatt einen Timer zu verwenden :

ExecutorService executor = Executors.newSingleThreadExecutor(); Future future = executor.submit(new LongRunningTask()); try { f.get(30, TimeUnit.SECONDS); } catch (TimeoutException e) { f.cancel(true); } finally { service.shutdownNow(); }

Hier haben wir den ExecutorService verwendet, um den Arbeitsthread zu senden, der eine Instanz von Future zurückgibt , deren get- Methode den Hauptthread bis zur angegebenen Zeit blockiert. Nach dem angegebenen Timeout wird eine TimeoutException ausgelöst . Im Fang Block, unterbrechen wir den Worker - Thread durch Aufruf der Abbrechen - Methode auf dem F uture Objekt.

Der Hauptvorteil dieses Ansatzes gegenüber dem vorherigen besteht darin, dass ein Pool zum Verwalten des Threads verwendet wird, während der Timer nur einen einzelnen Thread (keinen Pool) verwendet .

3.3. Verwenden eines ScheduledExcecutorSercvice

Wir können ScheduledExecutorService auch verwenden, um die Aufgabe zu unterbrechen. Diese Klasse ist eine Erweiterung eines ExecutorService und bietet dieselbe Funktionalität mit mehreren Methoden, die sich mit der Planung der Ausführung befassen. Dies kann die gegebene Aufgabe nach einer bestimmten Verzögerung der eingestellten Zeiteinheiten ausführen:

ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); Future future = executor.submit(new LongRunningTask()); executor.schedule(new Runnable(){ public void run(){ future.cancel(true); } }, 1000, TimeUnit.MILLISECONDS); executor.shutdown();

Hier haben wir mit der Methode newScheduledThreadPool einen geplanten Thread-Pool der Größe zwei erstellt . Die ScheduledExecutorService # Zeitplan Methode nimmt einen Runnable , einen Verzögerungswert, und die Einheit der Verzögerung.

Das obige Programm plant die Ausführung der Aufgabe nach einer Sekunde ab dem Zeitpunkt der Übermittlung. Diese Aufgabe bricht die ursprüngliche Aufgabe mit langer Laufzeit ab.

Beachten Sie, dass wir im Gegensatz zum vorherigen Ansatz den Hauptthread nicht durch Aufrufen der Future # get- Methode blockieren . Daher ist es der am meisten bevorzugte Ansatz unter allen oben genannten Ansätzen .

4. Gibt es eine Garantie?

Es gibt keine Garantie dafür, dass die Ausführung nach einer bestimmten Zeit gestoppt wird . Der Hauptgrund ist, dass nicht alle Blockierungsmethoden unterbrechbar sind. Tatsächlich gibt es nur wenige genau definierte Methoden, die unterbrechbar sind. Also, wenn ein Thread unterbrochen wird und ein Flag gesetzt ist, sonst nichts passieren wird , bis es eine dieser unterbrechbare Methoden erreicht .

Beispielsweise sind Lese- und Schreibmethoden nur dann unterbrechbar, wenn sie für Streams aufgerufen werden, die mit einem InterruptibleChannel erstellt wurden . BufferedReader ist kein InterruptibleChannel . Also, wenn der Thread verwendet es eine Datei zu lesen, ruft Interrupt () auf diesen Thread in dem blockierten Leseverfahren hat keine Wirkung.

Wir können jedoch nach jedem Lesen in einer Schleife explizit nach dem Interrupt-Flag suchen. Dies gibt eine angemessene Sicherheit, um den Thread mit einer gewissen Verzögerung zu stoppen. Dies garantiert jedoch nicht, dass der Thread nach einer bestimmten Zeit gestoppt wird, da wir nicht wissen, wie viel Zeit ein Lesevorgang dauern kann.

On the other hand, the wait method of the Object class is interruptible. Thus, the thread blocked in the wait method will immediately throw an InterruptedException after the interrupt flag is set.

We can identify the blocking methods by looking for a throwsInterruptedException in their method signatures.

One important piece of advice is to avoid using the deprecated Thread.stop() method. Stopping the thread causes it to unlock all of the monitors that it has locked. This happens because of the ThreadDeath exception that propagates up the stack.

Wenn sich eines der zuvor von diesen Monitoren geschützten Objekte in einem inkonsistenten Zustand befand, werden die inkonsistenten Objekte für andere Threads sichtbar. Dies kann zu willkürlichem Verhalten führen, das sehr schwer zu erkennen und zu begründen ist.

5. Schlussfolgerung

In diesem Tutorial haben wir verschiedene Techniken zum Stoppen der Ausführung nach einer bestimmten Zeit sowie die Vor- und Nachteile der einzelnen Techniken kennengelernt. Den vollständigen Quellcode finden Sie auf GitHub.