Advanced Java Services | Executors.newSingleThreadScheduledExecutor |
Die Methode newSingleThreadScheduledExecutor() gibt den Typ des Interfaces ScheduledExecutorService zurück. Damit hat man alle Methoden von ExecutorService und die 4 Methoden dieses Interfaces zur Verfügung. Ein Blick in den Quellcode zeigt lediglich einen Konstruktoraufruf:
public static ScheduledExecutorService newSingleThreadScheduledExecutor() { return new DelegatedScheduledExecutorService (new ScheduledThreadPoolExecutor(1)); }
Die statische innere Klasse DelegatedScheduledExecutorService bekommt also einen vollwertigen ScheduledThreadPoolExecutor dessen ThreadPool die Größe eins hat und daher mit nur einem einzigen Thread arbeitet. Die statische innere Klasse DelegatedScheduledExecutorService reicht aber nur die Methoden aus den Interfaces nach außen.
/** * A wrapper class that exposes only the ExecutorService methods * of an ExecutorService implementation. */ static class DelegatedExecutorService extends AbstractExecutorService { private final ExecutorService e; DelegatedExecutorService(ExecutorService executor) { e = executor; } public void execute(Runnable command) { e.execute(command); }
Damit erfährt man, mit welcher Queue und welchem Threadpool ein ScheduledThreadPoolExecutor per Default arbeitet. Die BlockingQueue<Runnable> ist eine statische innere Klasse ScheduledThreadPoolExecutor$DelayedWorkQueue
/** * Specialized delay queue. To mesh with TPE declarations, this * class must be declared as a BlockingQueue<Runnable> even though * it can only hold RunnableScheduledFutures. */ static class DelayedWorkQueue extends AbstractQueue<Runnable> implements BlockingQueue<Runnable> {
Die ThreadFactory ist wie bei allen Executoren die packageprivate statische innere Klasse DefaultThreadFactory der Factoryklasse Executors.
Die Methode schedule() steht für Runnable und Callable zur Verfügung. Die Verwendung ist einfach
private static void singleShotWith_newSingleThreadScheduledExecutor() { /** Returntyp von schedule ist ScheduledFuture> ScheduledFuture> schedule(Runnable command, long delay, TimeUnit unit) Creates and executes a one-shot action that becomes enabled after the given delay. Der dritte Parameter sagt, wie der zweite Parameter zu interpretieren ist. */ ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor(); Runnable ru = () -> System.out.println(Thread.currentThread().getName() + " Action at: " + Instant.now()); System.out.println("start singleThreadScheduledExecutor at " + Instant.now()); ses.schedule(ru, 3, TimeUnit.SECONDS); ses.shutdown();// shutDown auch bei singleshot notwendig }
Die Ausgabe (z.Bsp)
start singleThreadScheduledExecutor at 2016-03-20T11:13:51.198Z pool-1-thread-1 Action at: 2016-03-20T11:13:54.214Z
Alle schedule()-Methoden funktionieren nach dem Schema starten nach einer gewissen Zeit. Das Starten um eine bestimmte Zeit wird nicht durch Methoden unterstützt. Hierfür holt man sich entweder die Quartz-Library oder man schreibt sich einfache Situationen selbst. Das folgende Beispiel verwendet einen Hilfsthread um die Uhrzeit abzufragen. In diesem wird alle 10 Millisekunden die aktuelle Uhrzeit mit der Startzeit verglichen. In diesem Fall braucht man keinen ScheduledExecutorService, man kann das Runnable direkt starten.
public static void singleShotAt(Runnable ru, LocalTime startTime) { Runnable r = new Runnable() { @Override public void run() { if (ru==null || startTime==null) throw new NullPointerException(); ExecutorService es = Executors.newSingleThreadExecutor(); LocalTime now = LocalTime.now(); if (now.isAfter(startTime)) return; System.out.println("Executor created at : " + LocalTime.now()); while(LocalTime.now().isBefore(startTime)) { try { TimeUnit.MILLISECONDS.sleep(10); } catch(InterruptedException ex) { throw new RuntimeException(ex); } } es.execute(ru); es.shutdown(); } }; new Thread(r).start();
Dieser Methode kann man nun ein Runnable übergeben und es um eine bestimmte Uhrzeit starten.
private static void singleShotWith_newSingleThreadScheduledExecutor() { Runnable ru = () -> System.out.println(Thread.currentThread().getName() + " Action at: " + LocalTime.now()); singleShotAt(ru, LocalTime.now().plusSeconds(5)); }
Die Ausgabe
Executor created at : 12:35:27.533 pool-1-thread-1 Action at: 12:35:32.526
Bei wiederholten Ausführungen kann man zwischen zwei Konfigurationen wählen, entweder scheduleAtFixedRate oder scheduleWithFixedDelay.
Diese Variante arbeitet nach einem einfachen Algorithmus. Egal wie lange die Ausführung eines Threads dauert, die Pausen zwischen den Threads sind immer gleich lang, auf englisch fixed delay.
Das folgende Beispiel zeigt dieses Verhalten. Die zu wiederholeneden Threads dauern entweder 2 Seklunden oder 4 Sekunden, die Pause zwischen dem Ende eines Threads und dem Beginn des neuen Threads dauern 3 Sekunden.
private static void periodicShot_FixedDelay_newSingleThreadScheduledExecutor() { ScheduledExecutorService stse = Executors.newSingleThreadScheduledExecutor(); System.out.println("singleThreadScheduledExecutor starts at " + LocalDateTime.now()); Runnable ru = () -> { System.out.println(Thread.currentThread().getName() + " start: " + Instant.now()); Random r = new Random(); int dur = 2 + 2*r.nextInt(2); // der thread dauert entweder 2 sekunden oder 4 sekunden System.out.println(Thread.currentThread().getName() + " duration: " + dur); try{ TimeUnit.SECONDS.sleep(dur);}catch(InterruptedException ex){} System.out.println(Thread.currentThread().getName() + " end: " + Instant.now() + "\n"); }; // startet den Task nach 2 Sekunden, Pause zwischen den Threads ist 3 Sekunden stse.scheduleWithFixedDelay(ru, 2, 3, TimeUnit.SECONDS); // main muss warten, da shutDown() sofort beendet. try { stse.awaitTermination(30, TimeUnit.SECONDS ); } catch(InterruptedException ex) { ex.printStackTrace(); } System.out.println("shutdown"); stse.shutdown(); }
Eine mögliche Ausgabe. Man sieht, daß die Pausen sehr genau eingehalten werden.
Executors-ScheduledExecutorService singleThreadScheduledExecutor starts at 2016-03-21T10:25:14.441 pool-1-thread-1 start: 2016-03-21T09:25:16.451Z pool-1-thread-1 duration: 2 pool-1-thread-1 end: 2016-03-21T09:25:18.466Z pool-1-thread-1 start: 2016-03-21T09:25:21.467Z pool-1-thread-1 duration: 2 pool-1-thread-1 end: 2016-03-21T09:25:23.467Z pool-1-thread-1 start: 2016-03-21T09:25:26.468Z pool-1-thread-1 duration: 4 pool-1-thread-1 end: 2016-03-21T09:25:30.468Z pool-1-thread-1 start: 2016-03-21T09:25:33.470Z pool-1-thread-1 duration: 2 pool-1-thread-1 end: 2016-03-21T09:25:35.470Z pool-1-thread-1 start: 2016-03-21T09:25:38.471Z pool-1-thread-1 duration: 4 pool-1-thread-1 end: 2016-03-21T09:25:42.471Z shutdown end main
In dieser Variante sollen die Threads immer in konstanten Abständen starten. Ist die Dauer eines Threads kürzer als die Periode so ist das kein Problem. Was aber passiert, wenn ein Thread länger als eine Periode dauert? Startet dann der neue Thread bevor der alte zu Ende ist um die Periode einzuhalten oder wird gewartet bis der überlange Thread zu Ende ist? und wenn gewartet wird, wird dann bei kürzeren Threads versucht die Zeit wieder aufzuholen? Das kommende Beispiel klärt das Verhalten!
Wir brauchen das obige Beispiel nur in einer Zeile abzuändern.
private static void periodicShot_FixedRate_newSingleThreadScheduledExecutor()
{
ScheduledExecutorService stse = Executors.newSingleThreadScheduledExecutor();
System.out.println("singleThreadScheduledExecutor starts at " + LocalDateTime.now());
...
stse.scheduleAtFixedRate(ru, 2, 3, TimeUnit.SECONDS);
...
}
Der Ablauf zeigt, daß zum einen immer auf das Ende des Threads gewartet wird, was dann zu Verzögerungen führt. Andrerseits wird bei kürzer dauernden Threads sofort der nächste gestartet um die Zeit wieder reinzuholen. Auf der rechten Seite sieht man in grün die ideale Periode. M +1 etwa wird eine Verspätung von einer Sekunde angezeigt. Man sieht deutlich wie versucht wird, die Verspätungen aufzuholen. Erst der sechste Thread startet wieder pünktlich.
Executors-ScheduledExecutorService singleThreadScheduledExecutor starts at 2016-03-21T11:30:47.814 pool-1-thread-1 start: 2016-03-21T10:30:49.824Z // 49 +0 pool-1-thread-1 duration: 2 pool-1-thread-1 end: 2016-03-21T10:30:51.839Z pool-1-thread-1 start: 2016-03-21T10:30:52.824Z // 52 +0 pool-1-thread-1 duration: 4 pool-1-thread-1 end: 2016-03-21T10:30:56.824Z pool-1-thread-1 start: 2016-03-21T10:30:56.824Z // 55 +1 pool-1-thread-1 duration: 4 pool-1-thread-1 end: 2016-03-21T10:31:00.824Z pool-1-thread-1 start: 2016-03-21T10:31:00.824Z // 58 +2 pool-1-thread-1 duration: 2 pool-1-thread-1 end: 2016-03-21T10:31:02.824Z pool-1-thread-1 start: 2016-03-21T10:31:02.824Z // 01 +1 pool-1-thread-1 duration: 2 pool-1-thread-1 end: 2016-03-21T10:31:04.824Z pool-1-thread-1 start: 2016-03-21T10:31:04.824Z // 04 +0 pool-1-thread-1 duration: 2 pool-1-thread-1 end: 2016-03-21T10:31:06.825Z pool-1-thread-1 start: 2016-03-21T10:31:07.824Z // 07 +0 pool-1-thread-1 duration: 2 pool-1-thread-1 end: 2016-03-21T10:31:09.824Z pool-1-thread-1 start: 2016-03-21T10:31:10.824Z // 10 +0 pool-1-thread-1 duration: 4 pool-1-thread-1 end: 2016-03-21T10:31:14.824Z pool-1-thread-1 start: 2016-03-21T10:31:14.824Z // 13 +1 pool-1-thread-1 duration: 4 shutdown end main pool-1-thread-1 end: 2016-03-21T10:31:18.824Z // 16 +2
Hier der Beginn des obigen Ablaufs graphisch.