並行処理プログラミングにおいてJavaのメモリモデルを深く掘り下げたことがなかったのでちょっと本気出してみます。間違っているところあるかもしれません。ツッコミいただければ適宜訂正させていただきます。可変データへのアクセスを同期化JavaでActorっぽいものを作ってみるで使っていた、canceledフラグはvolatileという修飾子を付けて宣言しています。簡単にいうとと固有ロックを使わずに、フィールドを読み込むスレッドが、最後に書きこまれた値を見えるようにするためのvolatile修飾子です。共有された可変データへのアクセスを同期する」の例で考えてみたいと思います。}}スレッドが開始したら1秒後に終了するようなプログラムです。このプログラムは終了しそうで終了しません。みなさんは、このプログラムは約1秒動作して、その後、メインスレッドがstopRequestedをtrueに設定することで、バックグラウンドのスレッドのループが終了すると期待されるかもしれません。私のマシンでは、プログラムは決して終了しません。バックグラウンドのスレッドは永久にループし続けます。「なんと!」と思うかもしれませんが、、同期なしでは、仮想マシンが次のコードに変更する*1ことは許容されています。}これでは確かに終了しませんね。本書では解決策のひとつとして、以下のように固有ロック(synchronized)を使えばよいとしています。stopRequestedに設定する。stopRequestedを取得する。}}stopRequestedの値の読み込みと書き込みはアトミックな操作なのに、固有ロック(synchronized)で同期化する必要があるの?と思うかもしれません。それと、書き込み時だけ同期化すればいいんじゃない?と思うのですが、そうではありません。本書でも、次のように書き込みだけではなく、読み込み時にもスレッドから値がきちんと見えるようにしなければならないと言及しています。書き込みメソッド(requestStop)と読み込みメソッド(stopRequested)の両方が同期されていることに注意してください。書き込みメソッドだけを同期するだけでは十分ではありません。実際、読み込み操作と書き込み操作の両方が同期されていなければ、同期は何の効果もありません。JMM)を理解すれば理由がわかります。そのJMMの正しい理解を得るには、かなりの難易度ですが、Java言語仕様第3版(JLS3)を読むのがよいです。Specificationそれと、以下の記事も参考になりますが、これも難易度が高い。次のこれがわかるレベルになると、他人に並行処理プログラミングを任せられないレベルだとか。CookbookJMMがなぜ必要か「Java並行処理プログラミング」にもJMMの解説があります。こっちがわかりやすいかもしれません。見るそもそもなんで、JMMが必要か。P381の「16-1-1・プラットホームのメモリモデル」から引用。提供し、JVMはJMMとプラットホームのメモリモデルの違いを吸収するためにメモリバリヤを随所に挿入します。ハードウェアアーキテクチャ固有のメモリモデルというのはそれぞれに違いがあります。その違いをOSやコンパイラ、ランタイム、プログラム*2が違いを吸収し、スレッドセーフを実現しなければなりません。JavaではJMMがその差異を吸収してくれるわけです。JMMは、キャッシュのような存在に見えますが、実際にはキャッシュだけはなく、レジスタ、その他ハードウェア、コンパイラによる最適化などを含めた抽象的な概念を指します。ローカル・メモリとか、ローカル・キャッシュとかいうようです。加えて、メモリバリヤ(以下、メモリバリア)とは何かということですが、以下がわかりやすいかも。CPUには、性能最適化策としてアウト・オブ・オーダー実行を行うものがあり、メモリのロード命令やストア命令を含めて順序を入れ替えて実行する。この命令の並べ替えは、ひとつのスレッドの中で一般に暗黙のうちに行われるが、マルチスレッドプログラムやデバイスドライバでは慎重に制御しない限り予測不能の動作を生じる原因となる。順序性の制限の方法はハードウェア依存であり、そのアーキテクチャによって定義される。アーキテクチャによってはいくつかのバリアを用意して、それぞれ異なった順序性制限を実現している場合がある。そもそもCPUは性能の最適化でメモリのロード(読み込み)やストア(書き込み)の順序を入れ替えて実行するが、並行処理では問題が生じるので、メモリバリアという順序の制限を掛けることをしているということですね。JMMのメモリバリアについては、順序性の制限だけでなく、キャッシュのフラッシュまたは無効化、ハードウェアの書き込みバッファのフラッシュや実行パイプラインの停止などの、可視性保証も行うようです。Cookbookで読むとよいです。メモリモデルはなぜ必要か?」で、以下のような説明があります。3;このときメモリモデルは、次のような疑問に答えます:aVariableをリードするスレッドに値3が見える(スレッドが確実に値3をリードする)ためにはどんな条件が必要か?通常の変数への代入です。これを同期化なしでこの変数を別のスレッドから値が見えるかどうかという質問です。これは馬鹿げた疑問のように見えますが、しかし同期化が不在なら、いろんな理由で、あるスレッドがほかのスレッドの操作結果をすぐには見られない、または永遠に見られないことがあります。コンパイラが、ソースコードに書かれている常識的な順序ではない順序で命令をつくり出すことがあります。変数をメモリではなくプロセッサのレジスタに保存することもあります。プロセッサは複数の命令を並行に実行したり、コンパイラが作ったコードとは違う順序で命令を実行することがあります。キャッシュの介在によって、変数へのライトが主記憶にコミットされる順序が変わることがあります。プロセッサローカルなキャッシュに保存された値がほかのプロセッサから見えないこともあります。いった要因によって、変数の最新の値がスレッドから見えなかったり、ほかのスレッドで起きているメモリアクションが実際とは違う順序で見えたりします。ーいずれも、適切な同期化をしなかった場合です。同期化しない場合は、メモリモデルによって、スレッド間で変数が見えるか(可視性)どうか、変数に対する命令(ロード/ストア)の順序変更(リオーダー)が入れ替わるなど、が起こり得るということですね。固有ロック(synchronized)長くなりましたが、以下のStopThreadの固有ロック(synchronized)をかけた二つのメソッドに話を戻します。stopRequestedに設定する。stopRequestedを取得する。...}Java言語仕様の8.3.1.4のvolatile修飾子の解説で、固有ロック(synchronized)について以下のように触れています。returns.こういった無秩序な振る舞いを避けるには,メソッドone,twoをsynchronizedとして宣言(38.4.3.6)する方法がある:(中略)これにより,メソッドoneとメソッドtwoの並列実行が抑止され,メソッドoneが制御を返す前にi,jの共有値が双方とも更新されることを保証できる。この前後の文章を読んでもらうとわかるのですが、volatile修飾子には値を読み込むスレッドが、最後に書きこまれた値を見えるようにする効果があります。それと同じ効果が固有ロック(synchronized)にもあると記述されています。以下のドキュメントの方がもっと分かりやすいかもしれません。固有ロック(synchronized)はモニターという同期化の仕組みを利用していますが、そのモニターの機能の一部として、「synchronizedのブロックに出る際に、ローカルキャッシュをメインメモリに吐き出すことを要求し、同期化ブロックに入る際にはローカルキャッシュを無効化し、メインメモリを見にいく」動作を行うようです。JMMのメモリバリアのことですね。関連モニター解放の一部としてスレッドが同期化ブロックから出る時には、JMMはローカル・プロセッサーのキャッシュをメイン・メモリに吐き出すように要求します。同じように、同期化ブロックに入る際のモニター取得の一部として、(次に行われる読み込みがローカル・キャッシュではなく直接メイン・メモリに行くように)ローカル・キャッシュは無効化されます。あるスレッドが、対象のモニターに保護された同期化ブロック期間中に変数を書き込み、同じモニターに保護された同期化ブロック期間中に別のスレッドがその変数を読み出す時には、その変数への書き込みは読み取り側のスレッドから見えることが、この過程によって保証されるのです。同期化がない場合には、JMMはこの保証をしません。複数のスレッドが同じ変数にアクセスする時には必ず同期化(またはその弟分のvolatile)を使う必要があるのはこの理由からです。前述したように、requestStopメソッドでstopRequestedメソッドの両方で、synchronizedを指定しなければ最新の値を見ることができないのが、この理屈からわかります。同期化だけの目的だけなら、この二つのメソッドのコードはアトミックに動作するので同期化は不要なのですが、スレッド間で変数の値が見えるようにする、つまり可視性を保証するために固有ロック(synchronized)使うということですね。volatile修飾子それでは、固有ロック(synchronized)の弟分の、volatile修飾子を使った例も見てみましょう。そのコードが以下です。}}最初の例のstopRequestedフィールドにvolatile修飾子を付加しただけです。Java言語仕様において、そのvolatile修飾子が持つ意味は以下です。Javaのメモリ・モデル(17)はすべてのスレッドから見てその変数の値の整合性が保たれることを保証する。スレッドからの見て、「変数への書き込みや読み込みの整合性が保証される」のがvolatile修飾子の特徴です。メモリバリアが機能するわけです。volatile修飾子を付けた変数を揮発性変数とも呼びますが、「Java並行処理プログラミング」のP44「3-1-4・揮発性変数」にも解説があります。...揮発性変数へのアクセスはロックを実行せずスレッドをブロックしないので、synchronizedに比べると軽い同期化の仕組みです。volatileには排他制御がないので固有ロック(synchronized)より軽量です。今日のほとんどのプロセッサのアーキテクチャの上では、揮発性変数のリード(変数から読む)は不揮発性変数のリード(最適化されている場合主にレジスタやキャッシュから読む)よりも、ほんのわずかに高価な程度です。揮発性変数と不揮発性変数(finalでもvolatileでもない)の値を読み込むコストは、揮発性変数の「ほんのわずかに高価という程度」とのことです。P146「あまり利用するケースのない修飾子」にもvolatile修飾子について簡単な説明がありました。わかりやすい説明だと思いました。見る固有ロック(synchronized)とvolatile修飾子のどちらを使えばよいのか可視性を保証するために、固有ロック(synchronized)とvolatile修飾子のどちらを使えばよいのでしょうか。固有ロック(synchronized)はロックの争奪が伴うのですが、volatileでは排他制御が伴わないのでロックの争奪とは無縁です。この点はメリットです。「Java並行処理プログラミング」で、揮発性変数の可視性についてこんな言及があります。揮発性変数の可視性効果の及ぶ範囲は、その揮発性変数自身より広いです。スレッドAが揮発性変数に書き込みをして、次にスレッドBがその変数を読むと、Aがその揮発性変数に書き込む前にAにとって可視だったすべての変数の値が、Bにとって可視になります。そこでメモリの可視性という点では、揮発性変数に書き込むことはsynchronizedブロックを出ることに似ていて、それを読むことはsynchronizedブロックに入ることに似ています。可視性に関して、揮発性変数にあまり頼りすぎないほうがいいでしょう。あるステートの可視性をたまたま揮発性変数に頼っているコードは、明確にロックを使っているコードに比べて、不注意なリライトやメンテナンス作業で壊れやすく、読んでも分かりづらくなります。確かに揮発性変数がオブジェクトへの参照だった場合は、可視性の範囲が固有ロック(synchronized)より比較的に広くなりそうです。それでいて、その変数に読み書きすることは、可視性という点において、synchronizedブロックに入ったり出たりするのと同等の効果が発生するわけですね。ロックの争奪の影響を受けない代わりに、揮発性変数の扱いを注意深くする必要があります。揮発性変数を採用する基準は以下のようにするとよいとしています。揮発性変数は、次の条件をすべて満たすときだけ使ってください:その変数への書き込みが変数の現在値に依存しない。一つのスレッドだけが値を更新する。その変数がほかのステート変数とともに不変項に関与していない。その変数がアクセスされるとき、ほかの理由でロックが必要とされない。使うための条件」でも同様のことがが示されています。使い方としてはやはり限定的なのかなという印象。揮発性変数は、それを使ったほうが同期化ポリシーの実装と検証をしやすいときだけ使ってください。正しさを検証するために可視性に関するややこしい推理を要するときには、揮発性変数を使わないようにしましょう。いうことなので、原則的には固有ロック(synchronized)で検討し、ロック争奪で性能が劣化する場合や、volatileを使ったほうが変数の可視性がわかりやすい時に使うとよいのではないかと思います。まとめると、マルチスレッドで、固有ロック(synchronized)はアトミック性と可視性を、volatile修飾子の揮発性変数は可視性だけを保証するということです。次回のエントリ(若干先になるかも)では、JSR-133のメモリバリアとfinalフィールドについて掘り下げてみたいと思います。VMが行う巻き上げという最適化です。OS上のC言語のプログラムでは、そこまで物理的なメモリモデルを意識したことがないけど、組み込み機器や、PCでもアセンブラとかならあり得るかな。Sourcingを学ぶための教材ServiceとDCIについて月別アーカイブはてなブログをはじめよう!j5ik2oさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。