最近のCPUはデュアルコアは当たり前、デスクトップPCでさえクアッドコアを使用できる時代になりました。

このような時代の流れを先行するかのごとく、Javaでは当初よりスレッドを使った並行プログラミングが可能でした。とはいうものの、Threadクラスを使いこなすのはなかなか難しいというのも事実です。

そこで、J2SE 5.0では並行プログラミング用のAPIとして、Concurrency Utilitiesが導入されました。Concurrency Utilitiesには大別して次のような機能を持っています。

  • タスクの非同期実行機構
  • 並行コレクション
  • ロック、シンクロナイザ
  • アトミック処理

Java SE 6ではConcurrency Utilitiesも強化されています。4つの機能のそれぞれが強化されているのですが、変更点はそれほど大きくありません。そこで、本連載ではタスクの非同期実行機能の変更点を取り上げます。

今週は非同期実行の概要を簡単に説明し、来週に変更点を紹介します。

タスクの非同期実行

J2SE 1.4までは何らかの処理を非同期に行うにはThreadクラスを明示的に使用する必要がありました。Concurrency UtilitiesではThreadクラスを明示的に使用することはありません。

その代わりに登場するのが、java.util.concurrent.Executorインタフェース、もしくはその派生インタフェースであるjava.util.concurrent.ExecutorServiceインタフェースです。

ExecutorインタフェースはRunnableインタフェースを実装したクラスで記述されるタスクを実行するexecuteメソッドを定義します。しかし、Executorインタフェースではタスクの実行方法を指定することはありません。

タスク実行の方法はExecutorインタフェースを実装したクラスによって決まります。たとえば、java.util.concurrent.ThreadPoolExecutorクラスであれば複数のスレッドで構成されるスレッドプールを用いてタスクを実行します。

たとえば、100ミリ秒間スリープしてその後、標準出力にメッセージを出力するというまったく役に立たないタスクをExecutorインタフェースを使って実行してみましょう。

サンプルのソース ExecutorSample.java

以下にタスクを実行する部分のコードを示します。

    public ExecutorSample() {
        // タスクを定義する
        Runnable task = new Runnable() {
            public void run() {
                try {
                    MILLISECONDS.sleep(100L);
                    System.out.println("スリープ終了");
                } catch (InterruptedException ex) {}
            }
        };
        
        // Executorオブジェクトの生成
        Executor executor = Executors.newSingleThreadExecutor();
        // タスクの実行
        executor.execute(task);
    }

タスクは青字で示したように、Runnableインタフェースを実装した無名クラスで定義しています。MILLISECONDSはjava.util.concurrent.TimeUnit列挙型で定義されており、ミリ秒を表します。ここでTimeUnit.MILLISECONDSと表記していないのはimport static文でTimeUnit.MILLISECONDSをインポートしているためです。

タスクの実行を行なう部分は赤字で示してあります。

Executorオブジェクトの生成には、ユーティリティクラスであるjava.util.concurrent.Executorsクラスを使用します。

Executorsクラスにはタスク実行のポリシーに応じた複数種類のExecutorオブジェクトファクトリメソッドが定義されています。ここでは、シングルスレッドでタスクを実行するというポリシーに基づいて、newSingleThreadExecutorメソッドを使用しました。このメソッドで生成されるExecutorオブジェクトは1つだけスレッドを作成し、そのスレッドでタスクを順々に実行します。

その他にも、指定されたスレッドをあらかじめ生成してタスク実行を行なうnewFixedThreadPoolメソッドや、タスクの実行状況に応じてスレッド数を増減させるnewCachedThreadPoolメソッドなどが提供されています。

このようにExecutorインタフェースを使用することで、非同期にタスクの実行を行なうことができます。

しかし、タスクとしてRunnableインタフェースを使用するため、非同期に実行したタスクの戻り値を得ることができません。また、タスクを途中でキャンセルすることもできません。

このような問題に対応したのが、Executorインタフェースを派生させたExecutorServiceインタフェースです。

ExecutorServiceインタフェースでは、戻り値を戻すことのできるjava.util.concurrent.Callableインタフェースを使用することができます。そして、非同期に実行されるタスクからは直接戻り値を取得することができないため、java.util.concurrent.Futureインタフェースを使用して戻り値を取得します。

Futureインタフェースにはタスクが実行中であるかを調べることや、実行途中のキャンセルを行なうことができます。

ExecutorServiceインタフェースも簡単なサンプルで動作を確かめてみます。

サンプルのソース ExecutorServiceSample1.java

このサンプルのタスクは前のサンプルと同様に100ミリ秒スリープして標準出力にメッセージを出力します。そして、タスクの終了時間を戻り値として返します。

    public ExecutorServiceSample1() {
        // タスクを定義する
        Callable<Date> task = new Callable<Date>() {
            public Date call() {
                try {
                    MILLISECONDS.sleep(100L);
                    System.out.println("タスク終了");
                } catch (InterruptedException ex) {}
                 
                return new Date();
            }
        };
        
        // ExecutorServiceオブジェクトの生成
        ExecutorService executor = Executors.newSingleThreadExecutor();
        // タスクの実行
        Future<Date> future = executor.submit(task);

        System.out.println("タスク実行");
        try {
            // タスクの結果を取得する
            Date time = future.get();
            System.out.println("タスク終了時間: " + time);
        } catch (InterruptedException ex) {
        } catch (ExecutionException ex) {
            System.err.println("タスクの実行に失敗しました");
        }
    }

Callableインタフェースは赤字で示したようにジェネリクスでパラメータ化されています。このパラメータはcallメソッドの返り値の型になります。ここでは、Dateクラスが指定されているので、callメソッドの戻り値の型はDateクラスになります。

前のサンプルのExecutorSampleクラスではExecutorオブジェクトをExecutorsクラスのファクトリメソッドで生成しました。実をいうと、このファクトリメソッドはExecutorServiceオブジェクトを生成して返します。そのため、ExecutorServiceオブジェクトを生成する部分は前のサンプルと同一です。

そして、タスクの実行にはexecuteメソッドではなく、青字で示したようにsubmitメソッドを使用します。submitメソッドの戻り値がFutureオブジェクトになります。

Futureインタフェースもジェネリクスでパラメータ化され、パラメータはsubmitメソッドの引数に使用したCallableオブジェクトのパラメータと同一になります。タスクの結果(Callableオブジェクトのcallメソッドの戻り値)は、Future#getメソッドで取得することができます。

getメソッドをコールしたときにタスクが終了していない場合、処理をブロックします。

これを実行してみましょう。

C:\>java ExecutorServiceSample1
タスク実行
タスク終了
タスク終了時間: Thu Sep 24 22:20:42 JST 2007

タスクが終了した後、時間が取得できていることが確認できます。

なお、前のサンプルも、このサンプルは実行したら終了しません。それは、Executorオブジェクトが次のタスク実行を待っている状態にあるためです。終了させるにはExecutorService#shutdownメソッドをコールするようにします。

        executor.shutdown();

shutdownメソッドは登録されたすべてのタスクが終了した後、Executorオブジェクトをシャットダウンします。

Futureインタフェースにはタスク実行のキャンセルなどの機能があるので、結果を返さないRunnableインタフェースで記述されたタスクでも使いたいところです。もう一度ExecutorServiceインタフェースのJavadocを見ると、submitメソッドにRunnableオブジェクトを引数にするものがあることが分かります。

これも試してみましょう。

サンプルのソース ExecutorServiceSample2.java

Runnableオブジェクトを引数にとるsubmitメソッドは2種類オーバロードされています。一方はRunnableオブジェクト以外の引数がなく、タスクの結果を扱わないものです。もう一方が第2引数をタスクの結果とします。

    public ExecutorServiceSample2() {
        Runnable task = new Runnable() {
            public void run() {
                try {
                    MILLISECONDS.sleep(2000L);
                    System.out.println("タスク終了: " + new Date());
                } catch (InterruptedException ex) {}
            }
        };
 
        // ExecutorServiceオブジェクトの生成
        ExecutorService executor = Executors.newSingleThreadExecutor();
        // タスクの実行
        Future<Date> future = executor.submit(task, new Date());
 
        try {
            // タスク1の結果を取得する
            Date time = future.get();
            System.out.println("タスク結果: " + time);
        } catch (InterruptedException ex) {
        } catch (ExecutionException ex) {
            System.err.println("タスクの実行に失敗しました");
        }
 
        executor.shutdown();
    }

上記のコードではタスクの中で終了時間を出力しています(青地部分)。タスクの実行には実行時の時間をsubmitメソッドの第2引数にしています。

これで実行すると次のようになります。

C:\>java ExecutorServiceSample2
タスク終了: Thu Sep 24 23:00:08 JST 2007
タスク結果: Thu Sep 24 23:00:06 JST 2007

タスクの実行時刻が結果となっているので、終了時よりも2秒早くなっているのが確認できます。

このようにRunnableオブジェクトでもFutureオブジェクトと結びつけることが可能になります。

さて、今週はConcurrency Utilitiesの非同期実行について簡単に説明してきました。どこにもThreadクラスが出てこないのがお分かりだと思います。

タスクを記述するにはRunnableインタフェースもしくはCallableインタフェースを使用し、タスクの実行にはExectuorインタフェースもしくはExecutorServiceインタフェースが使用されます。また、タスクの結果などを扱うためにFutureインタフェースが使用されることを説明しました。

来週はこれをベースにJava SE 6での変更点について解説していきます。

 

著者紹介 櫻庭祐一

横河電機 ネットワーク開発センタ所属。Java in the Box 主筆

今月の櫻庭

食欲の秋、読書の秋、スポーツの秋と秋は何をするにも最適な季節です。そして、秋はイベントの季節でもあります。

櫻庭が幹事をしている日本Javaユーザグループ(JJUG)でも11月6日にクロスコミュニティカンファレンスを開催します。場所は東京国際フォーラム、参加費は無料です。

また、11月6日から8日までSun Tech Days 2007 in Tokyoが同じく東京国際フォーラムで開催されます。基調講演はJavaの父 James Gosling氏。これは聞き逃せません。

みなさまお誘いあわせの上、ぜひご参加ください。