Javaマルチスレッディングその2(UIスレッドとCallable)
というわけで前回の続きです。今回はUIスレッドという観点からJavaマルチスレッドの解説です。
Androidアプリケーションでは(というより多くのGUIアプリケーション開発においては)UI描画はメインスレッドでしか行えない制約があります。ですがすべての処理をメインスレッドで行うとUXは悪くなります。なので時間のかかる処理はサブスレッドで、UIに関わる処理はメインスレッドでということになります。
例えばHttpで何かを送信するだけというようなレスポンスのいらない処理であれば単に前回説明したようなRunnableとExecutorを用いた方法でいいのですが、インターネット経由で何かをダウンロードしたり、値を渡して時間のかかる演算をさせたりといったレスポンスを要求する処理の場合はCallableタスクを用いる必要があります。
CallableとFuture
CallableはRunnable同様タスクを表すインタフェースです。Executorに渡すことでcall()がサブスレッドで呼び出されます。Runnableと違う点は、call()には戻り値があることです。戻り値の型はCallableのジェネリクスで指定できます。サンプルとして呼び出されると2秒待機して値を返す単純なCallable実装クラスを作りました。
import java.util.concurrent.Callable; public class CallableTest { public static void main(String[] args) { } public static class MyCallable implements Callable<Integer> { @Override public Integer call() throws Exception { Thread.sleep(2000); return new Random().nextInt(); } } }
それではこれを呼び出していくのですが、ここで登場するのがFutureインスタンスです。FutureインスタンスはExecutor.submit(Callable)の戻り値として渡されます。
public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(4); MyCallable task = new MyCallable(); Future<Integer> response = executor.submit(task); try { System.out.println(response.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } }
Futureはそのタスクの実行状態と戻り値を管理するインタフェースです。主に使うのはget()とcancel(boolean)です。cancel(boolean)はそのタスクの実行を取り消します。引数のブール値はすでに実行中だった場合割り込んで止めるか見逃すかを指定できます。
get()はそのタスクの戻り値を返すのですが、このメソッドはもしまだそのタスクが完了していない場合(call()がまだ実行中)、get()を呼び出したスレッドを止めて待機します。つまり自動でjoinされるわけで、get()では必ず実行完了後の戻り値が得られます。(スレッド割り込みの例外と、タスク実行時の例外はキャッチしないといけませんが)
ですがこれをUIスレッドで行うと、非同期処理を行ったはいいが戻り値を得るためにUIスレッドを停止することになるわけで、これはよろしくありません。(場合によってはプログレスバーなどで進捗状態を示せば良いのでしょうが…)上のソースのままでは実行後get()してから約2秒間応答がなくなり、その後出力されるはずです。
これをどう解決するかということですがAndroidではHandlerという機能が用意されているのでそれを用います。申し訳ないですが通常のJavaでメインスレッドに無理やり処理を戻す方法は私はまだ知らないので良かったら教えて下さい。ここからはAndroidSDK前提での話になります。
まずはUIスレッドに最適化したHandlerを実装します。
public abstract class UiHandler extends Handler implements Runnable { public UiHandler() { super(Looper.getMainLooper()); } public UiHandler(Handler.Callback callback) { super(Looper.getMainLooper(), callback); } public boolean post() { return post(this); } public boolean postAtFrontOfQueue() { return postAtFrontOfQueue(this); } public boolean postAtTime(Object token, long uptimeMillis) { return postAtTime(this, token, uptimeMillis); } public boolean postAtTime(long uptimeMillis) { return postAtTime(this, uptimeMillis); } public boolean postDelayed(long delayMillis) { return postDelayed(this, delayMillis); } @Override public abstract void run(); }
そして先ほどの非同期処理部分をこうします。
public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(4); MyCallable task = new MyCallable(); final Future<Integer> response = executor.submit(task); executor.execute(new Runnable() { @Override public void run() { try { final int i = response.get(); new UiHandler() { @Override public void run() { System.out.println(i); } }.post(); } catch (Exception e) { e.printStackTrace(); } } }); }
get()をサブスレッドで呼び出すことによりUIスレッドの凍結を回避し、get()完了後にUIスレッドに処理を戻しています。printlnは別にサブスレッドでも呼べますがToast表示などではこのようにしなければ表示されません。
長くなりましたがJavaのCallableとFuture、そしてAndroidにおけるUIスレッドのハンドリングについては以上です。次回はロックオブジェクトとスレッドセーフコレクションについて解説してJavaMT解説は終わります。