この記事は、個人的なおさらいのための、Java Concurrency Utilitiesの一部を使ったサンプルとメモです。

目新しいものは特にありません。

記事内のサンプルとAPIドキュメント参照はJava7(Java SE 7)を基準にしていますが、Java Concurrency Utilities自体は一部を除いてJava5(Java SE 5.0)から使えるようになっています。

あと、いつものことですが、画像がありません。

追記(2014-01-05): この機能の呼称は (Java) Concurrency Utilities が公式で、この記事内の"Utility"というのは正確ではない＋混在していますのでご注意ください。ごめんなさい。

追記(2014-08-15): この記事は、キーワード"Java Concurrency Utilities"でGoogle検索した時に2番目に来ていたり、ブログ内でのアクセスが多かったりと、読んでいただく機会が多いみたいなので、読みやすくなるように改訂しました。"Utility"も"Utilities"に直しました。

ありがとうございます。

追記(2015-03-21)：Concurrency Utilitiesを中心にしたまとめを書いてみました。→Concurrency Utilitiesの「再」まとめ - Java8対応版

並列処理を比較的手軽に実現できるConcurrency Utilities マルチスレッドによる並列処理システムを構築するには、Javaの標準機能だけでもできないことはありませんが、安定した実現には多くの煩雑さと難解さが伴います。特に、シーケンシャルな処理では発生しないマルチスレッド特有の問題を考慮する必要があります。有名なところでは、スレッドアンセーフ（非スレッドセーフ）なオブジェクトの共有、デッドロックなどがあります。

そういった問題を回避しつつ、比較的容易にマルチスレッド関連機能を扱えるようにしてくれるのが、Concurrency Utilitiesです。

Concurrency Utilitiesは、JSR166で提案され、Java5から標準機能として使えるようになりました。 Concurrency UtilitiesのAPIは、 java.util.concurrent パッケージとそのサブパッケージに格納されています。

今回は、Concurrency Utilitiesのうち、いくつかをピックアップしておさらいします。







サンプルのための前提と準備 サンプルの実行結果は、JDK、JRE共に本家のJava7で、Windows7（32bit版）でコンパイル・実行したものです。他の環境でも動作するとは思いますが、細かな挙動が異なる可能性があります。

はじめに、それぞれのサンプルが小さくなるように、ユーティリティーなどを準備しておきます。また、同様の理由で、サンプルの一部では main メソッドとクラスは省略している箇所があります。 ユーティリティーメソッドクラス concurrent.Util log メソッド: ログ出力する sleep メソッド: スリープする（ InterruptedException を非チェック例外に変換） waitForSeveralSeconds : 数秒（１～10秒）、上記sleepでスリープする、スリープ前にログを出力

package concurrent; final class Util { private Util() { } static void log(Object o) { log( "%s" , o); } static void log(String fmt, Object ... args) { Thread currentThread = Thread.currentThread(); String datetime = String.format( "%1$tT.%1$tL" , System.currentTimeMillis()); datetime = "--:--:" + datetime.substring( 6 ); String threadName = String.format( "%s" , currentThread.getName()); System.out.printf( "%s [%s] %s%n" , datetime, threadName, String.format(fmt, args)); } static void sleep( long millis) { try { Thread.sleep(millis); } catch (InterruptedException ex) { throw new RuntimeException(ex); } } static void waitForSeveralSeconds() { final long millis = ( long )( 1000L * (Math.random() * 9 + 1 )); log( "waiting ... (it takes %d milliseconds)" , millis); sleep(millis); } }

SleepTask （ Runnable の実装クラス） 指定した時間スリープする

（ の実装クラス） package concurrent; import static concurrent.Util.*; final class SleepTask implements Runnable { private final long sleepMillis; SleepTask( long time) { this .sleepMillis = time; } @Override public void run() { log( "begin" ); sleep(sleepMillis); log( "end" ); } }

SlowDownCallableTask （ Callable<Integer> の実装クラス） waitForSeveralSeconds してから、引数の3倍の値を結果として返す

（ の実装クラス） package concurrent; import static concurrent.Util.*; import java.util.concurrent.*; final class SlowDownCallableTask implements Callable<Integer> { private final int input; SlowDownCallableTask( int input) { this .input = input; } @Override public Integer call() throws Exception { log( "input=%d" , input); waitForSeveralSeconds(); return input * 3 ; } }

スレッドプールを作ってタスクを処理させる - Executors , ExecutorService Executors , ExecutorService は、スレッドプールに関する機能を集約したものです。

スレッドを生成するのはそれなりに処理コストがかかるので、スレッドをあらかじめ作っておいたり、キャッシュして使いまわしたりします。それがスレッドプールです。

スレッドプールを使う場面では、キャッシュの管理だけではなく、タスクを空きスレッドに割り振って実行させたり、余ったスレッドを破棄したり、足りないスレッドを補充したり、同時実行数を制御したり、処理結果の受け渡しのための待ち合わせをしたりと、さまざまな機能が要求されます。これらを簡単に実現してくれるのが、スレッドプールのさまざまな操作を提供する ExecutorService インターフェイスと、 ExecutorService を生成する Executors ユーティリティークラスです。

まずは、ごく簡単な使い方を見てください。 ExecutorsSample1 ExecutorService pool; pool = Executors.newFixedThreadPool( 2 ); pool.execute( new SleepTask( 3000L )); pool.execute( new SleepTask( 2000L )); pool.execute( new SleepTask( 1000L )); pool.shutdown(); log( "end" ); ExecutorService#execute （ Executor#execute から継承されたメソッド）は、 Runnable のタスクとして実行するメソッドです。 Thread を使った時の new Thread(task).start() とするのと近いです。 ExecutorService#submit というメソッドもありますが、これについては後述します。 Executors.newXXXThreadXXX という名前のメソッドは、スレッドプール（型は ExecutorService ）を生成するファクトリーメソッドです。いくつかありますが、ここでは分かりやすいところで3種類を挙げています。 Executors.newCachedThreadPool は、足りない場合は自動的にスレッドを補充するスレッドプールを作ります。

Executors.newFixedThreadPool は、スレッド数が指定した数になるスレッドプールを生成します。タスクが投入された際にスレッドが足りない場合は、空きスレッドが出るまでタスクの実行は保留されます。

Executors.newSingleThreadExecutor は、スレッド数が1のスレッドプールを生成します。これは Executors.newFixedThreadPool(1) とほぼ同じです。

それでは、実行した結果を見てみましょう。 実行結果( ExecutorsSample1 ) - newFixedThreadPool(2) の場合 --:--:16.841 [pool-1-thread-2] begin --:--:16.841 [pool-1-thread-1] begin --:--:16.841 [main] end --:--:18.875 [pool-1-thread-2] end --:--:18.876 [pool-1-thread-2] begin --:--:19.875 [pool-1-thread-1] end --:--:19.876 [pool-1-thread-2] end 2つのスレッド、 pool-1-thread-1 と pool-1-thread-2 は同時に開始していますが、 pool-1-thread-2 は2番目に投入した2秒スリープするタスクを実行して先に終了するため、1番目のタスクで3秒スリープする pool-1-thread-1 より早く空きスレッドになります。その直後、 pool-1-thread-2 が保留となっていた3番目のタスクを処理しています。

main スレッドの処理は早々に終了しています。 ExecutorService#shutdown は、既に投入済みのタスクは処理が終わるまで待機してからスレッドを終了させます。

また、 shutdown を実行しないと、 main スレッドが終了してもJVMは終了しません。

shutdown の代わりに ExecutorService#shutdownNow （サンプルではコメントアウトになっている）を使うと、スレッドが実行中でも割り込み( InterruptException )を発生させてタスクを強制終了します。但し、確実にタスクを終了する保証はされない点に注意が必要です。

もうひとつ、デーモンスレッドだけになると、JVMは終了します。 main スレッドは「非デーモンスレッド」です。ということは、スレッドプールのスレッドがすべてデーモンスレッドになっていれば、 main スレッドが終了した時点で非デーモンスレッドがゼロになり、JVMは終了するということですね。（ Thread#setDaemon 参照。）

スレッドプールのスレッドをデーモンスレッドにするには、デーモンスレッドを生成する ThreadFactory を実装し、スレッドプール生成ファクトリーメソッドを実行する際に渡します。 ExecutorsSample2 ThreadFactory daemonThreadFactory = new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = Executors.defaultThreadFactory().newThread(r); thread.setDaemon( true ); return thread; } }; ExecutorService pool = Executors.newFixedThreadPool( 2 , daemonThreadFactory); pool.execute( new SleepTask( 3000L )); pool.execute( new SleepTask( 2000L )); sleep( 2500L ); log( "end" ); 実行結果( ExecutorsSample2 ) --:--:38.864 [pool-2-thread-1] begin --:--:38.864 [pool-1-thread-1] begin --:--:40.896 [pool-2-thread-1] end --:--:41.364 [main] end main スレッド以外はデーモンスレッドなので、 main スレッドが終了した直後にJVMが終了し、 pool-1-thread-1 の処理は完了しませんでした。











処理結果を待ち合わせる - Future 並列計算をするには、前項の ExecutorService を使えばできそうです。

ところが、 Runnable のタスクの Runnable#run メソッドは値を返すようになっていません。それに、計算が終わっていないのに結果を返すこともできません。計算が終わるまで待っていたら並列計算の意味がありません。どうやって値を返すようにすれば良いのでしょうか。

これを解決するには、 Future インターフェイスに関連した機能を使います。デザインパターンでいうところのFutureパターンです。 FutureTaskSample ExecutorService pool = Executors.newCachedThreadPool(); log( "start" ); Future<Integer> future = pool.submit( new SlowDownCallableTask( 7 )); Integer futureResult = future.get(); log( "result(Future): %d" , futureResult); FutureTask<Integer> task = new FutureTask <> ( new SlowDownCallableTask( 5 )); pool.submit(task); Integer futureTaskResult = task.get(); log( "result(FutureTask): %d" , futureTaskResult); pool.shutdown(); log( "end" );

最初の例は、 Callable （ java.util.concurrent.Callable インタフェース）の結果を Future （ java.util.concurrent.Future インターフェイス）として受け取って、 Future#get で結果が返されるまで待機します。

2番目の例は、 FutureTask （ java.util.concurrent.FutureTask クラス）を使ったものです。 FutureTask は、 Runnable と Future の役割を1つのクラスで賄うことができます。

Callable のタスクを実行するには、 ExecutorService#submit を使用します。



実行結果( FutureTaskSample ) --:--:04.220 [main] start --:--:04.270 [pool-1-thread-1] input=7 --:--:04.307 [pool-1-thread-1] waiting ... (it takes 3521 milliseconds) --:--:07.829 [main] result(Future): 21 --:--:07.830 [pool-1-thread-2] input=5 --:--:07.831 [pool-1-thread-2] waiting ... (it takes 7715 milliseconds) --:--:15.546 [main] result(FutureTask): 15 --:--:15.547 [main] end

同じグループのすべてスレッドを待ち合わせる（バリアー） - CyclicBarrier java.util.concurrent.CyclicBarrier クラスについて。

並列処理における「バリアー」という言葉は、「ロック」に比べると格段に馴染みの薄い用語ではないでしょうか。

Wikipediaでは「バリア (計算機科学)」*1という項目があります。

再利用できるので、 CyclicBarrier という名前になっているようです。 ※Java7以降であれば、 Phaser （ java.util.concurrent.Phaser クラス）も検討してみてください。

APIドキュメントに使用例がありますが、そのままでは動かせないコードなので、以下のサンプルはそのまま（と言っても main メソッドを用意＋インポート編成が必要...）で動かせるようにしています。細かい処理は削っています。 CyclicBarrierSample int threadCount = 5 ; ExecutorService pool = Executors.newFixedThreadPool(threadCount); final int [] results = new int [threadCount]; Runnable mergeTask = new Runnable() { @Override public void run() { log( "merge begin" ); log( "array: %s" , Arrays.toString(results)); int total = 0 ; for ( int result : results) total += result; log( "total: %d" , total); log( "merge end" ); } }; final CyclicBarrier cyclicBarrier = new CyclicBarrier(threadCount + 1 , mergeTask); for ( int i = 0 ; i < threadCount; i++) { final int index = i; Runnable calcTask = new Runnable() { @Override public void run() { log( "index=%d" , index); results[index] = (index + 1 ) * 3 ; waitForSeveralSeconds(); log( "await" ); try { cyclicBarrier.await(); } catch (InterruptedException | BrokenBarrierException ex) { log(ex); } } }; pool.execute(calcTask); } log( "await" ); try { cyclicBarrier.await(); } catch (InterruptedException | BrokenBarrierException ex) { log(ex); } log( "end" ); pool.shutdown(); 並列計算を簡略化した処理になっています。各ワーカースレッド*2が calcTask を実行し、結果を mergeTask で集計します。 CyclicBarrier#await を実行すると、そのスレッドは「バリアーポイント」で待機状態に入ります。待機スレッドが待ち合わせるスレッドの数に達すると、バリアーポイントを通過（「トリップ」する）して、後続の処理を行います。

CyclicBarrier のコンストラクタの第1引数は、待ち合わせるスレッドの数を指定します。このサンプルでは、ワーカースレッドに加えて main スレッドも待ち合わせに参加するので、ワーカースレッド数＋１をセットしています。第2引数には、トリップした時に実行されるアクション（バリアーアクション）を指定します。（無くても良い。）このアクションは、最後にバリアーに入ったスレッドにより実行されます。（ CyclicBarrier コンストラクタの説明参照。）

CyclicBarrier#await には、タイムアウトを設定するバージョン（ CyclicBarrier#await(long, java.util.concurrent.TimeUnit) ）もあります。

それにしても、こういう時の try-catch は邪魔ですね...



実行結果( CyclicBarrierSample ) --:--:27.424 [main] await --:--:27.424 [pool-1-thread-1] index=0 --:--:27.424 [pool-1-thread-2] index=1 --:--:27.424 [pool-1-thread-3] index=2 --:--:27.424 [pool-1-thread-4] index=3 --:--:27.424 [pool-1-thread-5] index=4 --:--:27.483 [pool-1-thread-2] waiting ... (it takes 8605 milliseconds) --:--:27.483 [pool-1-thread-3] waiting ... (it takes 9917 milliseconds) --:--:27.483 [pool-1-thread-4] waiting ... (it takes 3176 milliseconds) --:--:27.484 [pool-1-thread-1] waiting ... (it takes 8469 milliseconds) --:--:27.484 [pool-1-thread-5] waiting ... (it takes 2514 milliseconds) --:--:29.998 [pool-1-thread-5] await --:--:30.661 [pool-1-thread-4] await --:--:35.954 [pool-1-thread-1] await --:--:36.088 [pool-1-thread-2] await --:--:37.401 [pool-1-thread-3] await --:--:37.401 [pool-1-thread-3] merge begin --:--:37.402 [pool-1-thread-3] array: [3, 6, 9, 12, 15] --:--:37.402 [pool-1-thread-3] total: 45 --:--:37.403 [main] end --:--:37.403 [pool-1-thread-3] merge end 待ち合わせるスレッドの数は6です。この例では、 pool-1-thread-3 が最後にバリアーポイントに到達、「トリップ」し、 mergeTask は pool-1-thread-3 によって実行されていることが分かります。









