Java 8 Concurrency チュートリアルの第 1 部にようこそ。 このガイドでは、わかりやすいコード例を用いて、Java 8 での並行プログラミングを学びます。 これは、Java Concurrency API をカバーする一連のチュートリアルのうちの最初の部分です。
- Part 1: Threads and Executors
- Part 2: Synchronization and Locks
- Part 3: Atomic Variables and ConcurrentMap
Concurrency API は、Java 5 のリリース時に初めて導入され、その後、新しい Java のリリースごとに徐々に強化されてきました。 この記事で紹介されているコンセプトの大半は、古いバージョンの Java でも機能します。 しかし、私のコードサンプルはJava 8に焦点を当て、ラムダ式やその他の新機能を多用しています。
スレッドとランナブル
すべての最新のオペレーティング システムは、プロセスとスレッドの両方による同時実行をサポートしています。 プロセスは、通常、互いに独立して実行されるプログラムのインスタンスです。たとえば、Java プログラムを起動すると、オペレーティング システムは、他のプログラムと並行して実行される新しいプロセスを生成します。
Java は JDK 1.0 からスレッドをサポートしています。 新しいスレッドを開始する前に、このスレッドで実行されるコードを指定する必要がありますが、これはしばしばタスクと呼ばれます。 これは、Runnable
run()
は、次の例で示すように、単一のvoid no-argsメソッドを定義する機能的なインターフェイスです。
Runnable
は関数型インターフェースなので、Java 8 のラムダ式を利用して、現在のスレッド名をコンソールに表示することができます。 まず、新しいスレッドを開始する前に、メイン スレッド上で実行可能なプログラムを直接実行します。
コンソールに表示される結果は次のようになります。
あるいは、次のようになります。
同時実行のため、ランナブルが「done」と表示される前に呼び出されるのか、それとも「done」と表示された後に呼び出されるのかは予測できません。
スレッドは一定時間スリープ状態にすることができます。
上記のコードを実行すると、最初の print 文と 2 番目の print 文の間に 1 秒の遅延があることに気づくでしょう。 TimeUnit
は、時間の単位を扱うのに便利な列挙体です。
Thread
クラスでの作業は非常に面倒で、エラーが発生しやすいものです。 このような理由から、2004年にJava 5がリリースされたときに、Concurrency APIが導入されました。 この API は java.util.concurrent
パッケージに含まれており、並行プログラミングを処理するための便利なクラスが多数含まれています。
さて、Concurrency API の最も重要な部分の 1 つであるエクゼキュータ サービスについて詳しく見てみましょう。
エクゼキュータ
Concurrency API では、スレッドを直接操作するための高レベルな代替手段として ExecutorService
の概念を導入しています。 エクゼキュータは非同期タスクを実行することができ、通常はスレッドのプールを管理するので、新しいスレッドを手動で作成する必要はありません。 内部プールのすべてのスレッドは、リベンジ・タスクのために再利用されますので、アプリケーションのライフサイクルを通じて、1つのエクゼキュータ・サービスで必要なだけの同時タスクを実行することができます。
エクゼキュータを使用した最初のスレッドサンプルは次のようになります。
クラス Executors
は、さまざまな種類のエクゼキュータ サービスを作成するための便利なファクトリ メソッドを提供しています。
結果は上記のサンプルと似ていますが、コードを実行してみると、重要な違いに気づくでしょう。
ExecutorService
shutdown()
shutdownNow()
は実行中のすべてのタスクに割り込んでエクゼキュータを直ちにシャットダウンします。
これは、私が一般的に実行者をシャットダウンする好ましい方法です。
実行者は、現在実行中のタスクの終了を一定時間待つことで、ソフト的にシャットダウンします。
Callables and Futures
Runnable
Callable
という別の種類のタスクをサポートしています。 Callableはrunnableと同様に機能的なインターフェースですが、void
ではなく、値を返します。
以下のラムダ式は、1秒間スリープした後に整数を返す呼び出し可能なものを定義しています:
呼び出し可能なものは、ランナブルのように実行サービスに提出することができます。 しかし、callablesの結果はどうでしょうか? submit()
Future
型の特別な結果を返し、後で実際の結果を取得するために使用できます。
実行者に呼び出し可能なファイルを送信した後、まずisDone()
を介して未来の実行が既に終了しているかどうかを確認します。
メソッドを呼び出すと get()
現在のスレッドがブロックされ、実際の結果を返す前に呼び出しが完了するまで待機します。
フューチャーは、基盤となるエクゼキュータ サービスと緊密に結合しています。
エクゼキュータの作成方法が前の例と少し違うことにお気づきでしょうか。 newFixedThreadPool(1)
を使用して、サイズ1のスレッドプールでバックアップされたエクゼキュータサービスを作成します。
タイムアウト
future.get()
への呼び出しは、基礎となる callable が終了するまでブロックして待機します。 最悪の場合、callable は永遠に実行され、アプリケーションが応答しなくなります。
上記のコードを実行すると、TimeoutException
が生成されます。
この例外がスローされる理由は、もうお分かりかと思います。
InvokeAll
エクスクルーシブでは、invokeAll()
を介して複数の呼び出し可能なオブジェクトを一度に一括送信することができます。