目次


Javaの理論と実践

(若干) シンプルになった並行性

util.concurrentパッケージの紹介

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: Javaの理論と実践

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:Javaの理論と実践

このシリーズの続きに乞うご期待。

私たちの大半は、プロジェクトで必要になったとしても、XMLパーサーや、テキスト検索、検索エンジン、正規表現コンパイラー、XSLプロセッサー、あるいは、PDFジェネレーターなどのユーティリティーを、プロジェクトの一部として独自に作成することなど決して考えないでしょう。これらの機能が必要な場合は、市販やオープン・ソースの実装を使用して、そうしたタスクを実行させます。もっともなことですが、既存の実装は適切に機能し、また、簡単に利用することができ、独自のものを作成したとしても比較的利点が少ない (あるいはまったくない) わりには、多くの作業が必要となるでしょう。ソフトウェア・エンジニアとして、私たちは、巨人の肩の上に立つ (既存のものを活用する) というIsaac Newtonの熱意を共有していると信じたいところですが、そう望んでいる人もいますが、いつもそうとは限りません。(Richard Hamming氏は、Turing Awardの講義で、コンピューター科学者はむしろ「互いの足の上に立つ (既存のものを活用しない)」ことを好むと述べています)。

作り直しの理由を探る

しかし、ロギングやデータベース接続プーリング、キャッシング、タスク・スケジューリングなどの低レベルのアプリケーション・フレームワーク・サービス (ほぼすべてのサーバー・アプリケーションに必要) に関して、私たちは、これらの基本的なインフラストラクチャー・サービスは繰り返し作り直すものであると考えています。これは、なぜでしょうか。既存のオプションでは適切ではないことや、カスタム・バージョンのほうが、手元のアプリケーションに対して、より優れている、あるいは適していることが必ずしもその理由ではありません。実際、カスタム・バージョンは、広く利用されている汎用の実装よりも、開発の対象としたアプリケーションに対して適さないことが多く、質が低い場合もよくあります。たとえば、log4jは好きではないかもしれませんが、作業を適切に処理します。さらに、自作のロギング・システムは、log4jにない特定の機能を持っているかもしれませんが、ほとんどのアプリケーションに関して、既存の汎用の実装を利用するよりも、完全なカスタム・ロギング・パッケージを初めから作成する方が、それだけの価値があると言い張るのは難しいでしょう。それにもかかわらず、多くのプロジェクト・チームは、独自のロギングや接続プーリング、あるいはスレッド・スケジューリング・パッケージを繰り返し作成しています。

見た目はシンプルですが

XSLプロセッサーを自分で作成することを考えない理由の1つは、それが膨大な量の作業となるからです。ただし、上述の低レベルのフレームワーク・サービスは一見簡単そうに見えるので、独自のものを作成することはそれほど困難ではないように見えます。しかし、正確に行うのは、見た目より困難です。こうした特定のサービスを毎回作り直す主な理由は、あるアプリケーションで必要な機能が、多くの場合、初めは小さいものですが、他の無数のプロジェクトが有する同様の問題に直面するにつれ、拡大することです。その主張は通常次のようなものです。「完全なロギング / スケジューリング / キャッシング・パッケージは必要ありません。シンプルなものが必要なだけなので、それを実行するものを作成するだけです。そして、それは、特定のニーズに対応したものになるでしょう」。しかし多くの場合、作成したシンプルな機能ではすぐに足らなくなり、機能を次から次へと追加したくなり、最終的には完全なインフラストラクチャー・サービスを作成しています。その時点で、通常、既に作ってしまったものがいいものであろうとなかろうと、それから離れられなくなってしまっています。すでに独自のものを作成する全部のコストを支払っているので、汎用の実装へ移行する実際の移行コストに加え、「サンク・コスト」(投下済み費用)の壁も克服しなければならないでしょう。

並行性ビルディング・ブロックの貴重な発見

スケジューリングと並行性のインフラストラクチャー・クラスの作成は、明らかに見た目より困難です。Java言語は、wait()notify()、およびsynchronized という有益な、低レベルの同期プリミティブのセットを提供しますが、これらのプリミティブの使い方の詳細はわかりにくく、パフォーマンス、デッドロック、公平性、リソース管理、スレッド・セーフティーに関して回避すべき障害など多くの事柄があります。並行コードの作成は難しく、テストはさらに困難であり、専門家でさえ初めての場合は間違うことがあります。Concurrent Programming in Java (参考文献を参照) の著者であるDoug Lea氏は、ロックやmutex、キュー、スレッド・プール、軽量のタスク、効果的な並行コレクション、アトミック算術演算、その他並行アプリケーションの基本的なビルディング・ブロックなどの、優れた並行性ユーティリティーのフリー・パッケージを作成しました。このパッケージは通常util.concurrent と呼ばれ (実際のパッケージ名は長すぎるので)、Java Community Process JSR 166の下で標準化され、JDK 1.5ではjava.util.concurrent パッケージの基礎となる予定です。当面は、util.concurrent は、十分にテストされ、JBoss J2EEアプリケーション・サーバーなどの多くのサーバー・アプリケーションで使用されます。

すき間を埋める

mutexやセマフォー、ブロッキング、スレッド・セーフのコレクション・クラスなど、有益な、高レベルの同期ツールのセットが、中核となるJavaクラス・ライブラリーから明らかに欠落していました。synchronizationwait()notify()といったJava言語の並行性プリミティブは、ほとんどのサーバー・アプリケーションのニーズを満たすにはレベルが低すぎます。ロックを取得する必要がある場合、どうなるでしょうか。一定時間内にそれを取得しないとタイムアウトになるでしょうか。スレッドが中断されたらロックの取得を中止しますか。最大Nスレッドが保持できるロックを作成しますか。排他的書き込み付きの並行読み取りのような、マルチモードのロッキングをサポートしますか。あるいは、1つのメソッドでロックを取得し、別のメソッドでそれを解放しますか。組み込みロッキングは、これらをどれも直接的にはサポートしませんが、これらはすべて、Java言語が提供する基本的な並行性プリミティブ上に構築することができます。しかし、これを作る作業にはコツがいりますし、間違えやすいものです。

サーバー・アプリケーション開発者は、相互排除を実行し、イベントへの応答を同期化し、また、アクティビティー間でデータを通信し、非同期でタスクのスケジュールを作成するための簡単な機能を必要としています。Java言語がこの目的のために提供する低レベルのプリミティブは、使用が難しく、エラーが発生しやすいものです。util.concurrent パッケージは、ロッキング、ブロッキング・キュー、タスク・スケジューリングのためのクラス・セットを提供することによって、このすき間を埋めようとしています。これらのクラスは、共通のエラー・ケースを処理したり、あるいは、タスク・キューやプロセス中のワークによって消費されるリソースを制限したりする機能を提供します。

非同期タスクのスケジューリング

util.concurrent で最も広く使用されているのは、非同期イベントのスケジューリングを処理するクラスです。このコラムの7月の記事では、スレッド・プールとワーク・キューについて説明し、小さな作業単位のスケジューリングを行うために「Runnable のキュー」のパターンが多くのJavaアプリケーションによってどのように使用されているかを説明しました。

バックグラウンド・スレッドをforkしてタスクを実行するために、タスク用に単に新しいスレッドを作成したくなるものです。

new Thread(new Runnable() { ... } ).start();

この表記は魅力的で簡潔ですが、2つの重要な欠点があります。まず、新しいスレッドの生成には、一定のリソース・コストがかかる上、多くのスレッド (それぞれが短いタスクを実行し、その後終了する) を生成すると、JVMは、実際に有用な作業を行うよりも、スレッドの生成や破棄に、より多くの作業を行い、より多くのリソースを消費する場合があります。生成や破棄のオーバーヘッドがまったくないとしても、この実行パターンには、特定のタイプのタスクの実行で使用されるリソースをどのように制限できないという、さらに微妙な2つめの欠点があります。要求が突然殺到した場合に、一度に1,000ものスレッドが生成されるのをどのように防ぎますか。現実のサーバー・アプリケーションは、それ以上に注意深くリソースを管理する必要があり、一度に実行する非同期タスクの数を制限することが必要です。

スレッド・プールは、これらの問題を両方とも解決し、スケジューリング効率の向上とリソース使用の制限の利点を同時に提供します。プールされたスレッドでRunnable を実行するワーク・キューとスレッド・プールは簡単に作成できますが (7月のコラムのコード例で作成)、単に共有キューへのアクセスを同期化するより、効果的なタスク・スケジューラーを作成するためには非常に多くの作業が必要になります。現実のタスク・スケジューラーは、消滅するスレッドを処理し、不必要にリソースを消費しないように余分にプールされたスレッドを削除し、また、負荷に基づいてプール・サイズをダイナミックに管理し、キューに入ったタスクの数を制限しなければなりません。キューに入ったタスクの数の制限という最後の項目は、サーバー・アプリケーションが過負荷になった場合に、メモリー不足エラーによるクラッシュを防止するために重要です。

タスク・キューの制限には、ポリシーの決定が必要です。ワーク・キューがオーバーフローしたら、オーバーフローをどのように処理しますか。最新のアイテムを破棄しますか。最も古いアイテムを破棄しますか。キューでスペースが利用できるようになるまで、サブミットするスレッドをブロックしますか。サブミットするスレッドの新しいアイテムを実行しますか。実行可能なオーバーフロー管理ポリシーにはさまざまなものがあり、それぞれに利害得失があります。

Executor

Util.concurrent は、Runnable を非同期で実行するExecutor インターフェースを定義し、異なるスケジューリング特性を提供するExecutor のいくつかの実装も定義します。タスクを実行プログラムのキューに入れるのは極めて簡単です。

Executor executor = new QueuedExecutor();
...
Runnable runnable = ... ;
executor.execute(runnable);

最も簡単な実装はThreadedExecutor で、各Runnable に新しいスレッドを作成しますが、new Thread(new Runnable() {}).start() イディオムによく似て、リソース管理を提供しません。ただし、ThreadedExecutor には、1つの重要な利点があります。実行プログラムの構成を変更するだけで、異なる実行モデルに移行することができ、アプリケーション・ソース全体をじっくりと見渡して、新しいスレッドを作成するすべての場所を探す必要がありません。QueuedExecutor は、AWTやSwingのイベント・スレッドとよく似たもので、単一のバックグラウンド・スレッドを使用して、すべてのタスクを処理します。QueuedExecutor には、タスクがキューに入った順に実行されるという優れた性質があり、それらはすべて単一のスレッド内で実行されるので、タスクは共有データへのすべてのアクセスを必ずしも同期化する必要がありません。

PooledExecutor は、スレッド・プールの高度な実装であり、ワーカー・スレッドのプールでタスクのスケジューリングを提供するだけでなく、柔軟なプール・サイズの調整とスレッドのライフサイクル管理も提供します。これは、ワーク・キューのアイテムの数を制限して、利用可能なメモリーがキューに入ったタスクによってすべての消費されてしまうことを防ぎ、シャットダウンや飽和に関するさまざまなポリシーを適用できます (ブロック、廃棄、スロー、最も古いものを廃棄、呼び出し側のランインなど)。すべてのExecutor 実装は、実行プログラムのシャットダウン時にすべてのスレッドをシャットダウンするなど、スレッドの作成と取り外しを管理し、また、アプリケーションが希望すればスレッドのインスタンス化を管理できるよう、スレッド作成プロセスへのフックも提供します。これによって、たとえば、特定のThreadGroup にすべてのワーカー・スレッドを置いたり、記述名を付けることができます。

FutureResult

結果が後で必要となる場合に利用できるよう、プロセスを非同期で開始したいと思うかもしれません。FutureResult ユーティリティー・クラスによってそれを簡単に行うことができます。FutureResult は、実行に多少時間がかかり、別のスレッドで実行できるタスクを表し、FutureResult オブジェクトは、その実行プロセスのハンドルとして機能します。FutureResult を通して、タスクが完了したかどうか調べる、完了するのを待つ、そして、その結果を取り出すことができます。FutureResult は、Executor と組み合わせて使うことができます。FutureResult を作成して、その参照を保持したまま、Executor のキューに入れることができます。リスト1は、FutureResultExecutor を組み合わせた簡単な例で、イメージのレンダリングを非同期で開始し、他の処理を続行します。

リスト1. FutureResultとExecutorの実行
   Executor executor = ...
   ImageRenderer renderer = ...
       FutureResult futureImage = new FutureResult();
       Runnable command = futureImage.setter(new Callable() {
          public Object call() { return renderer.render(rawImage); }
       });
	// start the rendering process
       executor.execute(command);
       // do other things while executing
       drawBorders();
       drawCaption();
	// retrieve the future result, blocking if necessary
       drawImage((Image)(futureImage.get())); // use future

FutureResultとキャッシング

FutureResult を使用して、ロード・オンデマンドのキャッシュの並行性を改善することもできます。計算結果自体ではなく、FutureResult をキャッシュに入れることによって、キャッシュの書き込みロックを保持する時間を削減できます。最初のスレッドがアイテムをキャッシュに入れる速度が速くなるわけではありませんが、キャッシュにアクセスする他のスレッドを最初のスレッドがブロックしてしまう時間が削減されるでしょう。また、キャッシュからFutureResult を検索して取り出せるので、他のスレッドは、より早く結果を利用できるようになるでしょう。リスト2は、キャッシングにFutureResult を使用する例です。

リスト2. キャッシングを向上させるためのFutureResultの使用
  public class FileCache { private Map cache = new HashMap();
  private Executor executor = new PooledExecutor();
  public void get(final String name) {
    FutureResult result;
    synchronized(cache) {
      result = cache.get(name);
      if (result == null) {
        result = new FutureResult();
        executor.execute(result.setter(new Callable() {
          public Object call() { return loadFile(name); }
        }));
        cache.put(result);
      }
    }
    return result.get();
  }
}

このアプローチによって、最初のスレッドは、同期されたブロックにすばやく入って出ることができ、また、その他のスレッドは、最初のスレッドの計算結果を、最初のスレッドと同時に得ることができ、2つのスレッドが同じオブジェクトを計算することはありません。

まとめ

util.concurrent パッケージには数多くの有用なクラスが含まれており、そのいくつかは、みなさんが何度となく作成したことのあるクラスよりも優れたバージョンであることにお気付きになるでしょう。これらのクラスは、マルチスレッド化されたアプリケーションの数多くの基本的なビルディング・ブロックの、何度もテストされた、高パフォーマンスの実装です。util.concurrent は、JDK 1.5のjava.util.concurrent パッケージとなる並行性ユーティリティー・セットを作るJSR 166の出発点でした。しかし、JSR 166がリリースされるまで待つ必要はありません。次回の記事では、util.concurrent のカスタム同期クラスのいくつかについて説明し、さらに、util.concurrentjava.util.concurrent APIの異なる点について説明します。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=218569
ArticleTitle=Javaの理論と実践: (若干) シンプルになった並行性
publish-date=11012002