Advanced   Java   Services Executors.newSingleThreadScheduledExecutor Back Next Up Home


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.





Single shots

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

Single shots um eine bestimmte Uhrzeit

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





Wiederholte shots

Bei wiederholten Ausführungen kann man zwischen zwei Konfigurationen wählen, entweder scheduleAtFixedRate oder scheduleWithFixedDelay.


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.

java-schedule-with-fixed-delay.jpg

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

ScheduleAtFixedRate

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.

java-schedule-at-fixed-rate.jpg

Valid XHTML 1.0 Strict top Back Next Up Home