e.printStackTrace();}log("閉場");threadPool.shutdown();来客グループの人数と来場タイミングはランダムです。同時に20人しか入れないので、入場証を持っている人だけを入場させるようにします。退場時に入場証は返却します。残りの入場証が来客グループの人数に満たない場合は、3秒間だけ空くのを待ち、空かなければ入場を諦めます。閉場この実行結果では、11人のグループEが来場したタイミングで残りの入場証が2個しかないため、待機していますが、3秒待っても入場証が返却されないので、諦めて帰ってしまいました。この例では退場者チェックを行う役目をCountDownLatchが担っています。mainスレッドを待機させるために、入館したかどうかにかかわらず、帰った人をカウントして、カウントダウンします。次は、Exchangerを見てみましょう。これは、2つのスレッド間で値を交換するためのシンクロナイザーです。Exchangerは、遺伝アルゴリズムやパイプライン設計などのアプリケーションで便利です。ドキュメント次のサンプルコードの挙動を見れば、Exchangerがどのような動作をするのかはご理解いただけると思います。簡単に特徴を説明できる例が思い浮かばないので、また特に意味のない動作の例です。}}threadPool.shutdown();task1とtask2でそれぞれの配列の値をexchangeメソッドによって交換し合います。交換の際、時間差がありますので、早い方のスレッドが遅い方のスレッドをexchangeメソッドで待機します。task2結果=FG次はPhaserをみてみましょう。Phaserは、「バリアー」を実現するためのクラスです。Barrier)とは、同期方法の一つであり、ソースコード中でスレッドやプロセスがある箇所で停止し、他の全てのスレッドプロセスがバリアに到達するまで進行しないようなものを示す。Wikipediaというものを指します。CountDownLatchもバリアーとされています。ちなみに、Phaserは、Java7で追加されました。Phaserの特徴は、名前の通りフェイズに関係しています。各フェイズの開始をバリアーで待機させ、同時に開始させることができる、再利用可能なバリアーです。他の特徴としては、フェイズに参加するメンバー(スレッド)の数が開始後に変えられること、(ThreadGroupのような)親子関係のツリーを構築できること、監視はメンバー以外でも可能であること、などがあります。次のサンプルコードを見てください。});});sleep(6_000L);log("フェイズに途中参加");phaser.register();sleep(15_000L);log("途中参加したフェイズの開始を待機");phaser.arriveAndAwaitAdvance();log("終了");threadPool.shutdown();フェイズ1の開始をA,B,Cのスレッドが待機します。ランダム時間でスリープさせているのは、それぞれのスレッドで準備に要する時間が異なる様子を表しています。全てのスレッドがarriveAndAwaitAdvance()に到達すると、待機が解除されます。フェイズ2では、mainスレッドが途中参加し、mainスレッドを含めた4スレッドがarriveAndAwaitAdvance()に到達すると、待機が解除されます。到着したフェイズ番号=2CyclicBarrierは、Phaserをシンプルにしたような*4バリアー実装です。CountDownLatchは、Semaphoreで少し触れましたが、パーティーを特定しないタイプのバリアーです。設定したカウントをカウントダウンしていきカウントがゼロになった時に待機が解除される仕組みになっています。私はこのCountDownLatchがけっこうお気に入りです。CyclicBarrierとCountDownLatchの例については、前回の記事を御覧ください。してもこれらの既存のシンクロナイザーでは要件を充たせないのであれば、java.util.concurrent.locksパッケージのAPIを使用して、独自のシンクロナイザーを実装することもできます。実際、5つの既存のシンクロナイザーのうち、SemaphoreとCountDownLatchはjava.util.concurrent.locks.AbstractQueuedSynchronizerクラスを利用して実装されていますし、残りもjava.util.concurrent.locks.LockSupportクラスやjava.util.concurrent.locks.Lockインターフェイスの実装クラスを使用して実装されています。並行処理コレクションBlockingQueue以外にも、並列処理向きのコレクションが用意されています。代表的なものが、java.util.concurrent.ConcurrentHashMapクラスです。特徴のひとつとして、アトミックな操作があります。V)を実行する」のような操作(実際には、もっと複雑な実装になっています)ですが、これを他のスレッドに割り込まれること無く一貫して(=アトミックに)行うことができます。Java8では、ラムダ式に対応したメソッドが追加されています。BiFunction)は、putIfAbsentと似ていますが、こちらはキーの値を入力にしてラムダ式を評価した結果をputします。ConcurrentHashMapのアトミックな操作の詳細については、前回の記事も参照してください。Utilitiesでは、時間の単位はTimeUnit列挙型(java.util.concurrent.TimeUnit)が使われます。TimeUnitは主に、スケジューリングや、待ち合わせのタイムアウトの時間を設定する「タイミング・パラメーター」として使用されます。下記のメソッドで使用されます。TimeUnit)使い方の例は、タスク・フレームワークの項を参照してください。Utilities以外でも使用できます。TimeUnit)などがあります。アトミック値型とアトミック操作java.util.concurrent.atomicパッケージは、並列処理でのアトミックな値の更新・取得を行うためのAPI群です。細かく分けると、AtomicXXX、FieldUpdater、Adder/Accumulatorのグループ*5に分類できます。順番に見てみましょう。AtomicXXX(XXXにはIntegerなどが入る)はアトミック値型のクラスで、文字通り「アトミック」に更新ができる値です。「アトミック」とは、ひと言で言うと、複数のスレッドから値にアクセスした場合でも1スレッドの操作が一貫して行われることが保証される動作のことです。AtomicLong#getAndIncrement()は、値の取得と加算が他のスレッドに割り込まれること無く行われます。前回の記事ではこの辺りをもう少し詳しく書いていますので、併せてご参照ください。Java8では、アトミック値型にラムダ式に対応したメソッドがいくつか追加されています。AtomicLong#getAndUpdate(LongUnaryOperator)があります。アトミック値型と内包される型の関係は下表の通り。最後の2つは、参照型とプリミティブ型のペアーになっていて、マーキングができるようになっています。注:ペアーの表記は、Javaのものではなく、擬似コードです。AtomicReferenceFieldUpdaterがあります。FieldUpdaterを使うと、アトミックを使用していない任意のクラスのvolatileフィールドに「外付け」アトミック機能を組み込むことができます。リフレクションを使っているのでprivateフィールドでもOKですが、volatileは必須です。FieldUpdater以外からのvolatileフィールドへのアクセスをブロックすることはできませんので、FieldUpdaterからのみのアクセスになるような使い方に限定しないといけません。variables)です。それぞれlongとdoubleのための計4クラスあります。Adder/AccumulatorもAtomicIntegerなどと同じくNumberのサブクラスなので、値型の一種と言えるかもしれません。下記の制約がありますのでご注意ください。このクラスはNumberを拡張しますが、equals、hashCode、compareToなどのメソッドを定義しません。インスタンスでは変更が想定されているため、コレクション・キーとしては役立たないからです。ドキュメントまた、LongAdder,LongAccumulatorとAtomicLongには似たところがあります。ドキュメントにはこう書かれています。きめ細かい同期制御のためではなく統計収集などの目的に使用される共通の値が、複数のスレッドによって更新される場合、通常はAtomicLongよりもこのクラスをお薦めします。更新の競合が少ないときは、2つのクラスの特徴は似ています。競合が多いときは、期待されるスループットはこのクラスの方がかなり高くなります。容量消費も多くなります。ドキュメント並列ストリームを使ってlong値をカウントアップする処理を、LongAdderとAtomicLongで比べてみます。「容量消費」は度外視します。atomicLong);1000のスレッドがカウントアップを1,000,000回行い、合計が1,000,000,000になる処理です。並列処理はパラレルストリームを利用しています。カウント=1000000000このように、膨大な件数の集計処理を行う場合の速度に差が付きます。ロック・フレームワークjava.util.concurrent.locksパッケージには、ロックに関するAPIがまとめられています。パッケージの説明の日本語訳にちょっと違和感があります。Object#notifyとは別のロッキング・フレームワーク、という意味だと思うのですが...ざっと全体を見てみましょう。ReentrantLockクラスは、基本機能ではsynchronizedとほぼ同じことができ*6、かつ拡張可能なロック実装です。LockSupportクラスは、synchronizedやReentrantLockとは異なるロック操作(park/unpark)を提供します。この2つの詳細は後述します。ReentrantReadWriteLockは、Read-Writeロックの(ReadWriteLockインターフェイスの)デフォルト実装です。詳しくは、下記の記事をご覧ください。AbstractQueuedSynchronizerクラスは、新たにシンクロナイザーを実装するような場合に用意されたクラスです。シンクロナイザーの項でも触れましたが、SemaphoreとCountDownLatchは、AbstractQueuedSynchronizerを使用して実装されています。ブロック・ロック、および先入れ先出し(FIFO)待機キューに依存する関連シンクロナイザ(セマフォ、イベントなど)を実装するフレームワークを提供します。このクラスは、状態表現を単一の原子int値に依存する大半の種類のシンクロナイザの有用な基盤として設計されています。ドキュメントStampedLockクラスは、Java8で追加された新しい独立したロック実装です。long値で表現される「スタンプ」を使って、ロックなしでの読み取り(楽観的読み取り:optimistic-read)が可能な状態を管理できます。このクラスはLockインターフェイスを実装していません。下記の記事が参考になります。yohhoyの日記さて、冒頭で少し触れたReentrantLockとLockSupportについて、もう少し詳しく見ていきます。ReentrantLockクラスの説明には、次のことが書かれています。synchronizedメソッドおよび文を使用してアクセスする暗黙の監視ロックと同じ基本動作およびセマンティックスを使用し、かつ拡張機能を持つ、再入可能な相互排他Lockです。Object#notifyによるロックの基本動作とほぼ同じことが、APIであるReentrantLockを使って実現できる*7ということです。その上、synchronizedでは実現できない機能もいくつか持っています。ReentrantLockは、例えばConditionインターフェイスと組み合わせて、このように使うことができます。log("end");}).start();これは、下記のコードとほぼ同じ意味合いになります。log("end");}).start();最後に、LockSupportについて。LockSupportクラスは、すべてのメソッドがstaticです。park,unparkメソッドと、その亜種で構成されています。park/unparkというのは耳慣れない言葉ですね。クラスの説明にはこう書かれています。parkメソッドとunparkメソッドは、非推奨メソッドThread.suspendおよびThread.resumeをこのような目的に使用できなくなるような問題が発生しないスレッドをブロックおよびブロック解除するための効率的な手段を提供します。parkを呼び出しているあるスレッドと、そのスレッドのunparkを試みている別のスレッドの間の競合は、パーミットのために活発な状態を保持します。呼出し側のスレッドで割込みが発生し、かつタイムアウト・バージョンがサポートされている場合は、parkが復帰します。parkメソッドはまた「理由なしで」いつでも復帰する可能性があるため、一般には、復帰時に状態を再チェックするループ内で呼び出す必要があります。この意味で、parkはスピンにそれほどの時間を浪費しない「ビジー待機」の最適化として機能しますが、効果を発揮させるにはunparkとペアにする必要があります。ドキュメント基本的な使い方は、parkで待機しているスレッドをunparkで復帰させる、というものですが、説明にもあるとおり、parkメソッドは「理由なしで」いつでも復帰する可能性があるため、ループの中に閉じ込めておく必要があります。park状態で待機中のスレッドに割り込みをかけても復帰しますが、その際にInterruptedExceptionがスローされません。割り込まれたかどうかはThread#isInterrupted()を使って判断できます。ループを抜けて終了park側のスレッドは、AtomicBooleanを使ってwhileループに閉じ込めています。コードコメントにあるとおり、1回目のparkは取得済みパーミットを消費してそのまま通過できていますが、2回目は無視されています。私もまだ理解できていないところが多いですが、広く浅く網羅することはできたと思います。時間がかかってしまいました。少なくとも1/30の記事の時点には半分ほど書けていて、残り半分がしんどかった。参考になれば幸いです。(おわり)*1:Map-Reduceとは「似て非なるもの」のようです。*2:厳密には他のパターンもあります。詳しくはクラスのドキュメント参照。5.0で追加されたAPIです。*4:登場順は逆ですが。*5:公式の分類ではありません。*6:見かけの処理はほぼ同じですが同じものではありません。*7:厳密に言うとAPIだけでなくtry-finallyと組み合わせて、ですね。Java8対応版Tweet広告を非表示にする関連記事2016-02-26Javaでゲームパッドの入力をJInputを使って実現する突然、ゲームを作ってみたくなりました。(2)プロフィールid:argiusはてなブログをはじめよう!argiusさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。