Java ExecutorServiceの使い方、その1

2018年9月19日



Javaでは昔からThreadという非同期実行の仕組みがあるのだが、これとは別にJava1.5からExecutorServiceというものが追加された。これについて解説する。

単純な置き換え

まず、例外処理が面倒なのでsleepを以下のように定義する。

  static void sleep(long time) {
    try {
      Thread.sleep(time);
    } catch (InterruptedException ex) {}
  }

Threadバージョン

従来の単純処理を行いたい場合を考えてみる。

    new Thread(()-> {
      sleep(1000);
      System.out.println("done");
    }).start();
    System.out.println("started");

当然、以下の表示になり、startedからdoneの間に一秒かかる。

started
done

ExecutorServiceバージョン

これを置き換えるのは以下だ。

    ExecutorService service = Executors.newSingleThreadExecutor();
    service.execute(()-> {
      sleep(1000);
      System.out.println("done");
    });
    service.shutdown();
    System.out.println("started");

Threadの場合には、一度しか処理を与えることができないのだが、ExecutorServiceでは、何度もexecuteを呼び、処理を投与できるという。が、ここでは一度しか投与していない。そして、service.shutdown()は「投与の終了を宣言する」ものだそうだ。これが無いと投与された処理が終了してもスレッドが残ってしまい、結果メインスレッドが終了してもvmの実行は終了しないことになる。

しかし、これは誤解を招く名称だろう。もう少し別の名前にならなかったのだろうか。ともあれ、shutdown()はスレッドの実行自体とは無関係だ。あくまでも「投与受付を終了」するものだ。

そしてこれは間違いなく、Threadのバージョンと同じ表示になる。

joinするには?

処理の終了を待ちたいことがある。

Threadバージョン

従来の場合はjoin()を呼び出す。例えば以下のようにする。

※join()のInterruptedExceptionはとりあえず無視

    Thread thread = new Thread(()-> {
      sleep(1000);
      System.out.println("done");
    });
    thread.start();
    System.out.println("started");
    thread.join();
    System.out.println("joined");

この結果は以下になる。started/done間に一秒の待ちが発生する。

started
done
joined

ExecutorServiceバージョン

これを置き換えるのは以下だ。

※future.get()でのExecutionException, InterruptedExceptionはとりあえず無視。

    ExecutorService service = Executors.newSingleThreadExecutor();
    Future<?>future = service.submit(()-> {
      sleep(1000);
      System.out.println("done");
    });
    service.shutdown();
    System.out.println("started");
    future.get();
    System.out.println("joined");

先のThreadのバージョンと同じ結果になる。

※本来future.get()は値を返すものなのだが、この例ではnullしか返さない。

スレッドの処理結果を得たり、例外を得たりするには?

従来のThreadの場合、その処理結果を得たり、実行時例外を得たりするのは非常に面倒だった。基本的に処理として与えるRunnableでは、値を返したり例外を返したりすることができない。

Threadバージョン

public interface Runnable {
  void run();
}

この場合の例をあえて示すとすれば、次のような感じだろうか(大してテストはしていない)。

  public interface Procedure<R> {
    public R runit() throws Exception;
  }

  static class Wrapper<R> implements Runnable {
    Procedure<R>procedure;
    R returns;
    Exception exception;
    Wrapper(Procedure<R>procedure) {
      this.procedure = procedure;
    }
    public void run() {
      try {
        returns = procedure.runit();
      } catch (Exception ex) {
        exception = ex;
      }
    }
  }

  ....
  ....
    Wrapper<String>wrapper = new Wrapper<>(()-> {
      sleep(1000);
      return "done";      
    });    
    Thread thread = new Thread(wrapper);
    thread.start();
    System.out.println("started");
    thread.join();
    System.out.println("joined " + wrapper.returns);

ExecutorServiceバージョン

ExecutorServiceでは、このあたりも考えられている。例えば以下のようにする。

    ExecutorService service = Executors.newSingleThreadExecutor();
    Future<String>future = service.submit(()-> {
      sleep(1000);
      return "done";
    });
    service.shutdown();
    System.out.println("started");
    try {
      System.out.println(future.get());
    } catch (Exception ex) {
      ex.printStackTrace();
    }
    System.out.println("joined");

例外が発生しない場合の出力は以下となる。

started
done
joined

submitメソッドの違い

ExecutorServiceのsubmit呼び出しの違いに気づいたかもしれない。返り値や例外を取得しない場合のsubmitの呼び出しは、

    Future<?>future = service.submit(()-> {
      sleep(1000);
      System.out.println("done");
    });

だったが、特に返り値を取得する場合は、

    Future<String>future = service.submit(()-> {
      sleep(1000);
      return "done";
    });

になっているのである。この理由は、それぞれ異なる引数のsubmitメソッドが呼ばれているからだ。Javaはどちらのメソッドを呼ぶべきかわかっているのである。

前者は、返り値も例外も返さないRunnableを引数とする。

    Future<?> submit(Runnable task);

後者はCallableを引数としている。

    <T> Future<T> submit(Callable<T> task);

どちらが使用するかを、文脈に従って切り替えているわけである。この場合はreturn文があるかどうかになる。Callableの定義は以下になっている。

public interface Callable<V> {
    V call() throws Exception;
}