Java併発のFutureTaskの基本使用

3425 ワード

前回はJUCのFutureインターフェースについて話しましたが、最後にFutureTask、Copletion Serviceなどに言及しました。今回はまずJCIPの例を通してFutureTaskの基本的な使い方を説明し、次にどのようにFutureTaskのdone()を積載するかによってFutureTaskの機能を拡張します。
適用例:Final implemention of Memoizer
前の記事「Java併発のFutureインターフェース」では、単純にFutureインターフェースを使う限界について言及しましたが、その中のいくつかはFutureTaskで解決できます。ついでにネットで「FutureTask」を調べたら、上位の例はFutureTaskの役割を説明できないので、大家の労働成果を盗用して、「Java Concerency in Practice」の例を引用して、前編で紹介したListing 5.19です。ここで改めてJavaを勉強したいと提案しました。同時に勉強したい学生は必ずこの本をよく読みます。(JCIPを読んだことがある人はこの節を飛び越えます)
public class Memoizer<A, V> implements Computable<A, V> {
    private final ConcurrentMap<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
    private final Computable<A, V> c;

    public Memoizer(Computable<A, V> c) {
        this.c = c;
    }

    public V compute(final A arg) throws InterruptedException {
        while (true) {
            Future<V> f = cache.get(arg);
            if (f == null) {
                Callable<V> eval = new Callable<V>() {
                    public V call() throws InterruptedException {
                        return c.compute(arg);
                    }
                };
                FutureTask<V> ft = new FutureTask<V>(eval);
                f = cache.putIfAbsent(arg, ft);
                if (f == null) {
                    f = ft;
                    ft.run();
                }
            }
            try {
                return f.get();
            } catch (CancellationException e) {
                cache.remove(arg, f);
            } catch (ExecutionException e) {
                throw LaunderThrowable.launderThrowable(e.getCause());
            }
        }
    }
}
コード解析
上のコードを説明します。
余談ですが、上記のコードの中ではパラメータと変数の名前はとても簡単で、全部簡単なアルファベットで構成されています。実際のプロジェクトの開発では、このようなネーミングは使用しないでください。例では大丈夫です。
Memoizerという種類の仕事は、計算結果をキャッシュし、計算作業の重複提出を避けることです。これは例示的なコードですので、キャッシュの故障などを保護するロジックがありません。Memoizer類はComputtableインターフェースを実現しました。また、Memoizer類の構造方法でもComputtableの実現類をパラメータとして受け入れ、このComputtable実現類は具体的な計算作業を行います。
Memoizerのcomput方法は私達が注目する主要な部分です。while (true)を先にスキップして、11行目はキャッシュに既に計算タスクが存在しているかどうかを確認します。ない場合は、新しいタスクが提出される必要があります。そうでなければ、結果を取得すればいいです。if (f == null)に入り、Computable<A, V> cをまず一つのFutureTaskにカプセル化します。次いで、ConcurrentMap.putIfAbsent方法を呼び出して、計算タスクをキャッシュに入れる。この方法は原子操作であるため,戻り値はkeyであり,対応する元の値である。元の値が空であれば、計算タスクは本当に起動されます。さもなければ、繰り返し実行されます。最後に、計算結果をtryで呼び出します。while(true)およびcatchをドロップします。これらのコードは通常の流れですから、分かりやすいです。次にwhile(true)について説明します。計算ジョブがキャンセルされた後、再度任務を提出することができるという役割です。
次にキャッチ二つを言います。最初はcatch CancellationExceptionですが、この例ではFutureTaskはローカル変数で、cancelメソッドも起動していませんので、プログラムはここまで実行する機会がありません。
注意したいのは二番目のcatchです。二つ目のcatchはExecution Exceptionで、FutureTask内のRunnableまたはCallable実行時に投げられた異常はいずれもExecution Exceptionにパッケージされ、e.getCause()で取ることができる実際の異常です。明らかに、Execution Exceptionが発生した場合、計算は明らかに結果がないが、この例のコードでは、異常は簡単に再度投げ出されるだけである。これは計算結果が取得できなくなります。キャッシュはまだ占有されています。新しい計算タスクは提出できません。c.com mputeがべきなどの場合は、ここに提出されたタスクが異常を引き起こすことがありますので、合理的です。しかし、べき乗などではない場合は、例えば、インターネットの切断などの偶発的なイベントがあります。ここでは、計算タスクをキャッシュから削除して、新しいタスクを提出することができます。実際の応用では,具体的な異常の種類によって,異なる処理をする必要がある。c.computeが累乗かどうかを知らないならば、再試行の回数を制限することができます。再試行が制限を超えたら、キャッシュはもう削除されません。
直接運転できる拡張機能「Final implemention of Memoizer」コードはこちらをご覧ください。