BEA JRockit でのシン ロック、ファット ロック、再帰的ロック、および競合するロックについて
ここでは BEA JRockit での種々のロックについて説明します。
まず最も簡単な部分から説明しましょう。それは再帰的ロックです。再帰的ロックは次のような場合に発生します。
synchronized(foo) { // スレッドが初めてロックを取得する
synchronized(foo) { // ここでロックが再帰的に取得される
再帰的ロックの取得は何レベルか下のメソッド呼び出しでも発生することがありますが、これは問題ありません。再帰的ロックの取得は必ずしもまずいプログラミングとは言えません。少なくとも別のメソッドで行われる場合は問題になりません。
幸い、JRockit での再帰的ロックの取得は非常に高速です。実際、ロックを再帰的に取得するコストは、ほとんど無視できるくらい小さなものです。このことは、そのロックがもともとシン ロックとして取得されていてもファット ロックとして取得されていても変わりません (これらのロックについては、後ほど詳しく説明します)。
標高の高い薬の高血圧
ここで競合について少し触れておきましょう。競合が起きるのは、スレッドがロックを取得しようとしたとき、そのロックが取得できない状態にある (つまり、別のスレッドが保持している) 場合です。競合はパフォーマンスの点で常にコストがかかります。そのコストがどれくらいになるかは、いろいろな要因によって違ってきます。コストについては後でもう少し詳しく説明します。
パフォーマンスを重視するなら、できる限り競合を避ける努力をすべきでしょう。しかし、あいにく競合を避けることのできない状況が多いのです。複数のスレッドが同じ共有リソースに同時にアクセスしなければならないアプリケーションでは、競合は避けられません。しかし、設計をうまくすれば痛みを軽減することはできます。同期ブロックを使いすぎないように注意してください。激しく競合するロックを保持しているときに実行する必要のあるコードを最小限に抑えてください。競合しやすいとわかっているロックならば、1 つのロックで無関係なリソースを保護するのは避けてください。
原理上、アプリケーション開発者としてできるのはそれだけです。可能ならば競合を避けるようにプログラムを設計してください。JRockit のロック動作を一部変更するための実験的なフラグがいくつかありますが、これらを使用するのはやめた方がよいでしょう。デフォルト値は慎重に選ばれたものなので、これを変更しても、むしろパフォーマンスが悪化する可能性の方が高いからです。
とはいえ、アプリケーション開発者として JRockit が自分のアプリケーションに何をしているのか知りたいと思うのは当然です。そこで、JRockit におけるロック方式について、もう少し詳しく説明することにします。
Java のオブジェクトはすべて潜在的なロック (モニタ) です。この潜在性は、スレッドがそのオブジェクトの同期ブロックに入るとすぐに実際のロックとして顕在化します。このようにしてロックが「誕生」すると、それは「シン ロック」というロックになります。シン ロックには以下の特性があります。
不安障害や不安障害と
シン ロックの取得で最も高くつく部分は CAS (compare-and-swap) 操作です。これはアトミックな命令です。つまり、CPU 命令に関する限り、遅いということです。それでも、ロックの他の部分 (競合全般およびファット ロックの取得) に比べると非常に高速です。
おおむね競合しないロックについては、シンロックは非常に優れています。ロックがない場合に比べてわずかなオーバーヘッドがあるだけです。多くの (とりわけクラス ライブラリの) Java コードが同期化を多用するので、これは好ましいことです。
しかし、ロックが競合することになれば、たちまち何が最も効率的か単純に断じることはできなくなります。ロックの保持される時間が非常に短くて、JRockit がマルチ CPU (SMP) マシン上で動作しているなら、最善策は「スピンロック」です。スピンロックとは、ロックを必要としているスレッドがタイトなループの中で「スピン」しながら、そのロックがまだ取得されているかどうか継続的にチェックすることです。ということは、当然、ある程度のパフォーマンス ロスがあります。実際のユーザ コードが実行されるわけではなく、他のスレッドに振り向けることもできる時間を CPU が「浪費」しているからです。とはいえ、わずか数サイクルのうちに他のスレッドがそのロックを解放するなら、この方法をとる方が有利だと言えるでしょう。これが「競合するシン ロック」です。
ロックがすぐに解放されないのであれば、競合に対してこの方法を使用しているとパフォーマンスが悪くなります。その場合、ロックは「ファット ロック」に「引き上げ」られます。ファット ロックには以下の特性があります。
私の足と腰の痛みである
ファット ロックの競合に遭遇したスレッドは、自分自身をそのロックに対するブロッキングとして登録し、スリープ状態になります。つまり、OS によって割り当てられた時間の残りの分を放棄するということです。これは CPU が実際のユーザ コードの実行に使われることを意味しますが、それでもスピンロックに比べると余分なコンテキスト切り替えで高くつきます。スレッドがこれを行うと、「競合するファット ロック」が生じます。
競合している最後のスレッドがファット ロックを解放しても、そのロックは通常はファットのままです。たとえ競合がなくても、ファット ロックの取得はシン ロックの取得よりも高くつきます (ただし、ファット ロックまたはシン ロックをもう一方に変換するよりは安上がりです)。そのロックをシンにした方が有利だと JRockit が思えば (基本的に、競合が単なる「不運」であって、ロックが通常は競合しないなら)、そのロックを再びシン ロックに「引き下げる」かもしれません。
ロックについての特記事項 : ロックに関して wait/notify/notifyAll
が呼び出された場合、それは自動的にファット ロックに引き上げられます。そのため (それだけが理由ではありませんが)、1 つのオブジェクトに対して、通知のためのロックを他の何らかのロック方式とともに使用することは避けてください。
JRockit は複雑な一群のヒューリスティックを使って以下のような事柄も決定します。
これらのヒューリスティックは動的に適応できます。つまり、実行されている現実のアプリケーションに合うように自動的に変化するということです。
シン ロックとファット ロックの間での切り替えは JRockit によって自動的に行われ、アプリケーションのパフォーマンスが最大になるロックの種類が選ばれるので、シン ロックとファット ロックの間のパフォーマンス上の違いは、実はユーザが心配することではありません。この問題について一般的な解を示すのは不可能です。なぜなら、システムによって CPU の数、CPU の種類、システムの他の部分 (メモリやキャッシュ) などの要因に違いがあるからです。さらに、個々のシステムに関しても、よい解を示すことが非常に難しいのです。競合するシン ロックでのスピンで費やされる時間を正確に判定するのは特に注意を要することです。なぜなら、JRockit は諦める前にわずか数個のマシン命令を数回ループするだけなので、これをプロファイリングすると、この時間に大きな影響を与え、実際とは違ったパフォーマンス イメージを生み出すことになるからです。
まとめ : パフォーマンスを重視している場合は、ロックで競合が起きないようにアプリケーションを変更できるなら、ぜひそうしてください。競合を避けることができないなら、競合が起きるコードを最小限に抑えるよう努力してください。そうすれば、JRockit はアプリケーションを可能な限り高速に実行すべく全力を挙げるでしょう。JRA によってもたらされるロック情報をヒントとして利用してください。ファット ロックは激しくあるいは長い時間競合している可能性があります。コーディングを工夫して、ファット ロックで競合が最小限になるよう努めてください。
0 コメント:
コメントを投稿