kagamihogeの日記

kagamihogeの日記です。

The Java EE 7 TutorialのConcurrency Utilities for Java EEの章をテキトーに訳した

The Java EE 7 Tutorial56 Concurrency Utilities for Java EEのセクションを読んで訳した。

56 Concurrency Utilities for Java EE

このチャプターではJSR 236に記述されているConcurrency Utilities for Java EEについて解説しており、内容は以下のトピックの通りです。

  • Concurrency Basics
  • Main Components of the Concurrency Utilities
  • Concurrency and Transactions
  • Concurrency and Security
  • The jobs Concurrency Example
  • The taskcreator Concurrency Example
  • Further Information about the Concurrency Utilities

56.1 Concurrency Basics

コンカレンシー(Concurrency)*1とは、同時(並行)に二つ以上のタスクを実行する概念のことを指します。タスクは、メソッド(関数)、プログラムの一部分、もしくはそれと同等な他のプログラム、から構成されます。現在のコンピューターアーキテクチャーでは、マルチコアやシングルCPUでのマルチプロセスのサポートはごく普通となっています。

Javaプラットフォームは常にコンカレントプログラミングのサポートを提供し続けており、これはJava EEコンテナによって提供される多くのサービスを実装するための基礎となっています。Java SE 5では、コンカンレンシーのための高レベルAPIのサポートがjava.util.concurrentパッケージとして追加されました。

Java EE 7より前では、エンタープライズの開発者がコンカレンシーのユーティリティを安全で標準的な作法で使用できる仕様は存在しませんでした。Java EEのwebとEJBコンテナは、コンテナ管理のスレッドプールを使用してオブジェクトをインスタンス化します。それゆえ、Threadオブジェクトのインスタンス化にJava SE concurrent APIを使うことは強く非推奨でした。もし開発者が新しい(非コンテナ管理の)Threadオブジェクトを生成する場合、他のJava EEプラットフォームのサービス(たとえばトランザクションとセキュリティ)下にそのThreadが入ることを保障出来ません。

56.1.1 Threads and Processes

コンカレンシーの主な概念は二つあり、プロセス(processes)スレッド(threads)です。

プロセスは主にOS上で実行するアプリケーションに関連付けされています。プロセスはOSの下位層とやり取りしてメモリ等のリソースを割り当てるためにランタイムリソースを規定するもので、JVMはプロセスの一例です。

Javaプログラミング言語とプラットフォームではスレッドが主要な関心事です。

実行環境やOSのリソースを消費するために、スレッドはプロセスでいくつかの機能を共有します。しかし、スレッドはプロセスに比べるとかなり少ないリソース消費で生成できます。

スレッドは軽量なので、2~3コアで数GB程度のメモリを持つ近代的なCPUは単一のJVMプロセスで1000スレッドを動作可能です。生成可能な正確なスレッド数については、CPU・OS/利用可能なメモリ・JVMの正しい(チューニングされた)設定、に依存します。

コンカレントプログラミングは多くの問題を解決してアプリケーションのパフォーマンスを向上しますが、複数の実行ライン(スレッドやプロセス)は深刻な問題を引き起こす可能性も多々あります。そうした問題のいくつかを下記に挙げます。

  • デッドロック
  • スレッド飢餓(Thread starvation)*2
  • 共有リソースの同時実行アクセス*3
  • プログラムが不正確なデータを生成*4

56.2 Main Components of the Concurrency Utilities

コンカレントなリソース(Concurrent resources)とは、Java EEアプリケーションにコンカレンシーの互換性を提供するマネージド・オブジェクトのことです。GlassFIshサーバでは、コンカレントなリソースを設定し、servletEJBなどのアプリケーションコンポーネントで使用出来ます。コンカレントなリソースはJNDIルックアップやリソース・インジェクション経由で取得します。

concurrency utilitiesの主要なコンポーネントは以下の通りです。

  • ManagedExecutorService: managed executor serviceは、アプリケーションがサブミットされたタスクを非同期に実行するために使用します。タスクはコンテナによって開始・マネージドされるスレッド上で実行されます。コンテナのコンテキストはタスクを実行するスレッドに伝播します。
    たとえば、ManagedExecutorService.submit()をGenerateReportTaskなどのタスク を後で実行するためにサブミット可能で、その後、Futureのコールバックで結果が利用可能になった時点で値を取得します。
  • ManagedScheduledExecutorService: managed scheduled executor serviceは、アプリケーションが指定時刻にサブミットされたタスクを非同期に実行するために使用します。タスクはコンテナによって開始・マネージドされるスレッド上で実行されます。コンテナのコンテキストはタスクを実行するスレッドに伝播します。このAPIはユーザがアプリケーションでプログラミング的にタスク実行を日付/時刻指定でセット可能なスケジューリング機能を提供します。
  • ContextService: context serviceは、コンテナのコンテキストをキャプチャする動的プロキシ・オブジェクトを生成するために使用し、これによって、アプリケーションが後でコンテキスト内で実行したり、Managed Executor Serviceにサブミット出来るようになります。コンテナのコンテキストはタスクを実行するスレッドに伝播します。
  • ManagedThreadFactory: managed thread factoryは、アプリケーションがマネージド・スレッドを生成するために使用します。タスクはコンテナによって開始・マネージドされるスレッド上で実行されます。コンテナのコンテキストはタスクを実行するスレッドに伝播します。また、このオブジェクトは特定ユースケース用(カスタム・スレッド付の)カスタム・ファクトリの提供に使用可能です。これは、たとえば、固有ないしプロプライエタリなプロパティをオブジェクトに設定します。

56.3 Concurrency and Transactions

トランザクションの最も基本的な操作はコミットとロールバックですが、コンカレント処理を用いる分散環境では、コミットやロールバック操作が正常に処理されたことを保障するのは難しく、また、トランザクションが異なるスレッド・CPUコア・物理マシン・ネットワークに分散することも可能です。

そうした状況でロールバック操作が正常に実行されることを保証するのは極めて重要です。

Concurrency Utilitiesはjavax.transaction.UserTransactionを通してコンポーネントトランザクションのサポートと実装をするためにJava Transaction API (JTA)に依存しており、これは開発者にトランザクション境界の明確な管理の仕組みを提供しています。詳細な情報についてはJTA仕様を参照してください。

オプションで、コンテキストオブジェクトはトランザクションの開始・コミット・ロールバックが可能ですが、親コンポーネントトランザクションは取得出来ません。

以下のコードは、RunnableタスクでUserTransactionを取得と開始をしたあと、EJBやDBなどの他のトランザクションコンポーネントを呼び出して、コミットしています。

public class MyTransactionalTask implements Runnable {
 
   UserTransaction ut = ... // JNDIやDIによる取得
 
   public void run() {
 
       // トランザクションの開始
       ut.begin();
 
       // サービスやEJBの呼び出し
       myEJB.businessMethod();
 
       // XA JDBCドライバを使用するDBエンティティの更新
       myEJB.updateCustomer(customer);
 
       // トランザクションのコミット
       ut.commit();
 
   }
}

56.4 Concurrency and Security

Concurrency Utilities for Java EEは多くのセキュリティ上の決定をアプリケーションサーバの実装に委ねます。もしコンテナがセキュリティコンテキストをサポートする場合、コンテキストは実行スレッドに伝播可能です。ContextServiceはランタイムの振る舞いををいくつかサポートしており、security属性がenabledの場合はコンテナのセキュリティ・プリンシパルが伝播します。

56.5 The jobs Concurrency Example

このセクションでは、エンタープライズアプリケーションにおけるconcurrencyの基本機能の使用方法についての超基本的なサンプルの解説をします。Concurrency Utilities for Java EEの主要コンポーネントの一つ、Managed Executor Serviceを特に取り上げます。

このサンプルはpublic APIを公開するRESTful webを想定しており、このAPIは汎用ジョブ実行のサブミットを行います。これらのジョブはバックグラウンドで処理されます。各ジョブは実行の開始と終了時に"Starting"と"Finished" を表示します。また、バックグラウンド処理をシミュレートするために、各ジョブの実行時間は10秒です。

RESTfulサービスは二つのメソッドを公開します。

  • /token: 有効なAPIトークンを登録して返すGETメソッドを公開。
  • /process: クエリパラメータに実行したいジョブの識別子jobID、カスタムHTTPヘッダーX-REST-API-Keyにリクエストの有効チェック用のトークン、を受け取るPOSTメソッドを公開。

トークンはAPIで提供されるQuality of Service (QoS)の違いを出すために使用します。サービスリクエストにトークンを渡すユーザは同時に複数のジョブを処理可能です。ただし、トークンを渡さないユーザは同時に1ジョブのみ処理可能です。各ジョブは実行に10秒かかるので、トークンの無いユーザは10秒ごとに1度サービス呼び出しが可能です。トークンを渡すユーザはより高速に処理されます。

この違いを作るためには、各リクエストタイプごとに異なる2つのManaged Executor Servicesを使用します。

56.5.1 Running the jobs Example

2つのManaged Executor Servicesの追加設定をGlassFish Serverにしたあと、NetBeans IDEMavenのどちらかを使用して、jobsサンプルのビルド・パッケージング・デプロイ・実行を行う手順を説明します。

56.5.1.1 To Configure GlassFish Server for the Basic Concurrency Example

GlassFish Serverの設定は以下のステップで行います。

  1. http://localhost:4848で管理コンソールを開く。
  2. Resourcesを開く。
  3. Concurrent Resourcesを開く。
  4. Managed Executor Servicesをクリックする。
  5. Managed Executor Servicesページで、新規Managed Executor Servicesページを開くためにNewをクリックする。
  6. JNDI Nameフィールドに、high-priorityのManaged Executor Serviceを作成するためにMES_Highを入力し、以下の設定を使用する(他の設定はデフォルトのままで良い)。
    • Thread Priority: 10
    • Core Size: 2
    • Maximum Pool Size: 5
    • Task Queue Capacity: 2
  7. OKをクリックする。
  8. Managed Executor Servicesページで再度Newをクリックする。
  9. JNDI Nameフィールドに、low-priorityのManaged Executor Serviceを作成するためにMES_Lowを入力し、以下の設定を使用する(他の設定はデフォルトのままで良い)。
    • Thread Priority: 1
    • Core Size: 1
    • Maximum Pool Size: 1
    • Task Queue Capacity: 0
  10. OKをクリックする。

56.5.1.2 To Build, Package, and Deploy the jobs Example Using NetBeans IDE

  1. GlassFishサーバが実行中なことを確認(Starting and Stopping GlassFish Serverを参照)
  2. FileメニューからOpen Projectを選択
  3. Open Projectダイアログボックスから以下のディレクトリに移動。
    tut-install/examples/concurrency*5
  4. jobsフォルダを選択。
  5. Open Projectをクリック。
  6. Projectsタブでjobsプロジェクトを右クリックしてBuildを選択。
    このコマンドはアプリケーションをビルドしてデプロイします。

56.5.1.3 To Build, Package, and Deploy the jobs Example Using Maven

  1. GlassFishサーバが実行中なことを確認(Starting and Stopping GlassFish Serverを参照)
  2. ターミナルで以下に移動:
    tut-install/examples/concurrency/jobs
  3. アプリケーションをビルとしてデプロイするために以下のコマンドを実行。
    mvn install

56.5.1.4 To Run the jobs Example and Submit Jobs with Low Priority

low priorityのジョブをサブミットするユーザとしてこのサンプルを実行するには、以下のステップを実行します。

  1. webブラウザで、以下のURLを入力します。
    http://localhost:8080/jobs
  2. Jobs Clientページで、Enter a JobIDフィールドに1を入力、Enter a Tokenフィールドは空で、Submit Jobをクリックします。 以下のメッセージがページ下部に表示される筈です。

Job 1 successfully submitted

サーバログには以下のメッセージが記録されます。

INFO:   Invalid or missing token! 
INFO:   Task started LOW-1
INFO:   Job 1 successfully submitted
INFO:   Task finished LOW-1

これでlow priorityのジョブをサブミットしたことになります。これは、10秒間は別のジョブをサブミット出来ないことを意味します。もしサブミットを試行する場合、RESTful APIはservice unavailable (HTTP 503)をレスポンスし、ページ下部に以下のメッセージが表示されます。

Job 2 was NOT submitted

サーバログには以下のメッセージが記録されます。

INFO:   Invalid or missing token! 
INFO:   Job 1 successfully submitted
INFO:   Task started LOW-1
INFO:   Invalid or missing token! 
INFO:   Job 2 was NOT submitted
INFO:   Task finished LOW-1

56.5.1.5 To Run the jobs Example and Submit Jobs with High Priority

high priorityのジョブをサブミットするユーザとしてこのサンプルを実行するには、以下のステップを実行します。

  1. webブラウザで、以下のURLを入力します。
    http://localhost:8080/jobs
  2. Jobs Clientページで、Enter a JobIDフィールドに1から10を入力します。
  3. トークンを得るために"Get a token here"のhereリンクをクリックします。新しいタブにページが開かれてトークンが表示されます。
  4. トークンをコピーしてJobs Clientページに戻ります。
  5. Enter a Tokenフィールドにトークンをペーストし、Submit Jobをクリックします。 以下のようなメッセージがページ下部に表示される筈です。

Job 11 successfully submitted

サーバログには以下のメッセージが記録されます。

INFO:   Token accepted. Execution with high priority.
INFO:   Task started HIGH-11
INFO:   Job 11 successfully submitted
INFO:   Task finished HIGH-11

これでhigh priorityのジョブをサブミットしたことになります。これは、トークンによって複数のジョブをサブミット可能なことを意味し、low priorityをサブミットするときのジョブの10秒制限はありません。連続してトークン付きで3つのジョブをサブミットしたとすると、以下のようなメッセージがページ下部に表示されます。

Job 1 was submitted
Job 2 was submitted
Job 3 was submitted

サーバログには以下のメッセージが記録されます。

INFO:   Token accepted. Execution with high priority.
INFO:   Task started HIGH-1
INFO:   Job 1 successfully submitted
INFO:   Token accepted. Execution with high priority.
INFO:   Task started HIGH-2
INFO:   Job 2 successfully submitted
INFO:   Task finished HIGH-1
INFO:   Token accepted. Execution with high priority.
INFO:   Task started HIGH-3
INFO:   Job 3 successfully submitted
INFO:   Task finished HIGH-2
INFO:   Task finished HIGH-3

56.6 The taskcreator Concurrency Example

taskcreatorサンプルは、Concurrency Utilities for Java EEを使用してタスクの即時・定期・固定時間後、に実行する方法の例です。このサンプルには、ユーザがタスクを実行するためにサブミットとタスクごとのメッセージ情報を表示する、JavaServer Facesインタフェースがあります。タスクの即時実行にはManaged Executor Serviceを使用し、タスクの定期および固定時間後実行にはManaged Scheduled Executor Serviceを使用します。(これらのサービスについてはMain Components of the Concurrency Utilitiesを参照してください。)

taskcreatorは以下のコンポーネントから構成されます。

  • JavaServer Facesページ(index.xhtml)は三つの要素から構成されています。タスクをサブミットするためのフォーム、タスクの実行ログ、定期タスクをキャンセルするためのフォーム。このページはタスクの生成とキャンセルにAjaxリクエストをサブミットします。また、タスク実行ログを更新するためにJavaScriptを使用してWebSocketメッセージを受け取ります。
  • CDI managed bean(TaskCreatorBean)はJavaServer Facesページからのリクエストを処理します。このbeanは新しいタスクのサブミットと定期タスクをキャンセルするためにTaskEJBメソッドを実行します。
  • EJBTaskEJB)はresource injectionでexecutor serviceのインスタンスを取得して、実行するタスクをサブミットします。また、このbeanはJAX-RS webサービスのエンドポイントです。タスクはこのエンドポイントに情報メッセージを送信します。
  • WebSocketエンドポイント(InfoEndpoint)はクライアントにメッセージ情報を送信するためにEJBが使用します。
  • タスクのクラス(Task)はRunnableインタフェースを実装します。このクラスのrunメソッドTaskEJBwebサービス・エンドポイントに情報メッセージを送信し、1.5秒スリープします。

Figure 56-1はtaskcreatorサンプルのアーキテクチャを図で示しています。

Description of Figure 56-1 follows

TaskEJBクラスは以下のようにしてアプリケーションサーバからデフォルトのexecutor serviceオブジェクトを取得します。

@Resource(name="java:comp/DefaultManagedExecutorService")
ManagedExecutorService mExecService;

@Resource(name="java:comp/DefaultManagedScheduledExecutorService")
ManagedScheduledExecutorService sExecService;

TaskEJBsubmitTaskメソッドは以下のようにして実行したいタスクをサブミットするために上記のオブジェクトを使用します。

public void submitTask(Task task, String type) {
    /* アプリケーションサーバのmanaged executorオブジェクトを使用 */
    switch (type) {
        case "IMMEDIATE":
            mExecService.submit(task);
            break;
        case "DELAYED":
            sExecService.schedule(task, 3, TimeUnit.SECONDS);
            break;
        case "PERIODIC":
            ScheduledFuture<?> fut;
            fut = sExecService.scheduleAtFixedRate(task, 0, 8, 
                    TimeUnit.SECONDS);
            periodicTasks.put(task.getName(), fut);
            break;
    }
}

定期タスクでは、TaskEJBScheduledFutureオブジェクトの参照を保持し続けるので、ユーザはいつでもタスクキャンセル可能です。

56.6.1 Running the taskcreator Example

このセクションではNetBeans IDEMavenのどちらかを使用して、taskcreatorサンプルのビルド・パッケージング・デプロイ・実行を行う手順を説明します。

56.6.1.1 To Build, Package, and Deploy the taskcreator Example Using NetBeans IDE

  1. GlassFishサーバが実行中なことを確認(Starting and Stopping GlassFish Serverを参照)
  2. FileメニューからOpen Projectを選択
  3. Open Projectダイアログボックスから以下のディレクトリに移動。
    tut-install/examples/concurrency*6
  4. taskcreatorフォルダを選択。
  5. Open Projectをクリック。
  6. Projectsタブでtaskcreatorプロジェクトを右クリックしてBuildを選択。
    このコマンドはアプリケーションをビルドしてデプロイします。

56.6.1.2 To Build, Package, and Deploy the taskcreator Example Using Maven

  1. GlassFishサーバが実行中なことを確認(Starting and Stopping GlassFish Serverを参照)
  2. ターミナルで以下に移動:
    tut-install/examples/concurrency/taskcreator
  3. アプリケーションをビルとしてデプロイするために以下のコマンドを実行。
    mvn install

56.6.1.3 To Run the taskcreator Example

1.webブラウザーで以下のURLを開きます。

http://localhost:8080/taskcreator/

このページには、タスクをサブミットするためのフォーム・タスクの実行ログ・定期タスクをキャンセルするためのフォーム、があります。

2.Immediateタスク・タイプを選択し、タスク名を入力して、Submitボタンをクリックします。タスク実行ログに以下のようなメッセージが表示されます。

12:40:47 - IMMEDIATE Task TaskA finished
12:40:45 - IMMEDIATE Task TaskA started

3.Delayed (3 sec)タスク・タイプを選択し、タスク名を入力して、Submitボタンをクリックします。タスク実行ログに以下のようなメッセージが表示されます。

12:43:26 - DELAYED Task TaskB finished
12:43:25 - DELAYED Task TaskB started
12:43:22 - DELAYED Task TaskB submitted

4.Periodic (8 sec)タスク。タイプを選択し、タスク名を入力して、Submitボタンをクリックします。タスク実行ログに以下のようなメッセージが表示されます。

12:45:25 - PERIODIC Task TaskC finished run #2
12:45:23 - PERIODIC Task TaskC started run #2
12:45:17 - PERIODIC Task TaskC finished run #1
12:45:15 - PERIODIC Task TaskC started run #1

1つ以上の定期タスクを追加できます。定期タスクをキャンセルするには、フォームでタスクを選択してCancel Taskをクリックします。

56.7 Further Information about the Concurrency Utilities

concurrencyのより詳細な情報については、下記を参照してください。

関連エントリ

*1:素直に『同時並行性』とでも訳せばいいのだけど。この文書ではダサいの覚悟の上で『コンカレンシー』とそのままにする。

*2:たとえば、プライオリティの設定がマズくて、あるスレッドに一向に処理の優先順位が回ってこなくなること。『java並行処理プログラミング』p.245 10-3-1 飢餓状態 参照

*3:おそらく、Javaでいうところのsynchronizedを上手くつけないと甚大な速度低下を生むが、無いと共有リソースの状態が極めて不安定な謎バグを生むようなこと、とかを指してると思われる

*4:おそらく、たとえば、Servletインスタンス変数でカウンタを実装しようとする、アトミックでないが故に不正確なデータを生成するようになる状況のこと、と思われる

*5:https://java.net/projects/javaeetutorial/sources/svn/show/trunk/examples/concurrency

*6:https://java.net/projects/javaeetutorial/sources/svn/show/trunk/examples/concurrency