Javaでマルチスレッドの遅延初期化を実装したい。
ある種のコードがあります:
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null) {
Helper h;
synchronized(this) {
h = helper;
if (h == null)
synchronized (this) {
h = new Helper();
} // release inner synchronization lock
helper = h;
}
}
return helper;
}
// other functions and members...
}
そして、「ダブルチェックされたロックが壊れています」という宣言を受け取っています。
どうすればこれを解決できますか?
効果的なJavaの項目71:遅延初期化を慎重に使用するで推奨されるイディオムは次のとおりです。
インスタンスフィールドのパフォーマンスのために遅延初期化を使用する必要がある場合は、double-check idiomを使用します。このイディオムは、フィールドが初期化された後(項目67)、フィールドにアクセスするときのロックのコストを回避します。このイディオムの背後にある考え方は、フィールドの値を2回チェックすることです(つまり、名前double-check):1回はロックせずに、その後、フィールドは初期化されていないように見えます。 2番目のチェックでフィールドが初期化されていないことが示された場合のみ、呼び出しはフィールドを初期化します。フィールドが既に初期化されている場合、ロックは行われないため、フィールドを
volatile
として宣言することは重要です(項目66)。ここにイディオムがあります:// Double-check idiom for lazy initialization of instance fields private volatile FieldType field; private FieldType getField() { FieldType result = field; if (result != null) // First check (no locking) return result; synchronized(this) { if (field == null) // Second check (with locking) field = computeFieldValue(); return field; } }
このコードは少し複雑に見えるかもしれません。特に、ローカル変数の結果の必要性が不明確な場合があります。この変数が行うことは、フィールドがすでに初期化されている一般的な場合に、フィールドが1回だけ読み取られるようにすることです。厳密に必要なわけではありませんが、これによりパフォーマンスが向上する可能性があり、低レベルの並行プログラミングに適用される標準により洗練されています。私のマシンでは、上記の方法はローカル変数なしの明白なバージョンよりも約25%高速です。
リリース1.5より前は、揮発性修飾子のセマンティクスがそれをサポートするほど強力ではなかったため、ダブルチェックイディオムは確実に機能しませんでした[Pugh01]。リリース1.5で導入されたメモリモデルはこの問題を修正しました[JLS、17、Goetz06 16]。現在、ダブルチェックイディオムは、インスタンスフィールドを遅延初期化するための最適な手法です。静的フィールドにダブルチェックイディオムを適用することもできますが、そうする理由はありません。遅延初期化ホルダークラスイディオムの方が適しています。
これは、正しいダブルチェックロックのパターンです。
class Foo {
private volatile HeavyWeight lazy;
HeavyWeight getLazy() {
HeavyWeight tmp = lazy; /* Minimize slow accesses to `volatile` member. */
if (tmp == null) {
synchronized (this) {
tmp = lazy;
if (tmp == null)
lazy = tmp = createHeavyWeightObject();
}
}
return tmp;
}
}
シングルトンの場合、遅延初期化のためのはるかに読みやすいイディオムがあります。
class Singleton {
private static class Ref {
static final Singleton instance = new Singleton();
}
public static Singleton get() {
return Ref.instance;
}
}
ThreadLocal Byを使用したDCL Brian Goetz @ JavaWorld
DCLの何が問題になっていますか?
DCLは、リソースフィールドの非同期使用に依存しています。それは無害であるように見えますが、そうではありません。理由を確認するために、スレッドAが同期ブロック内にあり、ステートメントresource = new Resource();を実行するとします。スレッドBはgetResource()に入っているだけです。この初期化のメモリへの影響を考慮してください。新しいResourceオブジェクトのメモリが割り当てられます。 Resourceのコンストラクターが呼び出され、新しいオブジェクトのメンバーフィールドが初期化されます。また、SomeClassのフィールドリソースには、新しく作成されたオブジェクトへの参照が割り当てられます。
class SomeClass {
private Resource resource = null;
public Resource getResource() {
if (resource == null) {
synchronized {
if (resource == null)
resource = new Resource();
}
}
return resource;
}
}
ただし、スレッドBは同期ブロック内で実行されていないため、これらのメモリ操作は、スレッドAが実行する順序とは異なる順序で表示される場合があります。 Bがこれらのイベントを次の順序で確認する場合があります(また、コンパイラーはこのような命令を自由に並べ替えることもできます):メモリの割り当て、リソースへの参照の割り当て、コンストラクターの呼び出し。メモリが割り当てられてリソースフィールドが設定された後、ただしコンストラクタが呼び出される前に、スレッドBがやってきたとします。リソースがnullではないことを確認し、同期されたブロックをスキップして、部分的に構築されたリソースへの参照を返します。言うまでもなく、結果は期待も望まれもしません。
ThreadLocalはDCLを修正できますか?
ThreadLocalを使用して、DCLイディオムの明示的な目標、つまり共通コードパスでの同期なしの遅延初期化を達成できます。次のDCLの(スレッドセーフな)バージョンを検討してください。
リスト2. ThreadLocalを使用したDCL
class ThreadLocalDCL {
private static ThreadLocal initHolder = new ThreadLocal();
private static Resource resource = null;
public Resource getResource() {
if (initHolder.get() == null) {
synchronized {
if (resource == null)
resource = new Resource();
initHolder.set(Boolean.TRUE);
}
}
return resource;
}
}
おもう;ここで、各スレッドはSYNCブロックに入り、threadLocal値を更新します。その後はしません。そのため、ThreadLocal DCLは、スレッドがSYNCブロック内に1回だけ入るようにします。
同期とはどういう意味ですか?
Javaは、各スレッドを独自のローカルメモリを備えた独自のプロセッサ上で実行するかのように扱い、それぞれが共有メインメモリと通信して同期します。シングルプロセッサシステムでも、メモリキャッシュの影響と変数を格納するためのプロセッサレジスタの使用により、このモデルは理にかなっています。スレッドがローカルメモリ内の場所を変更すると、その変更は最終的にメインメモリにも表示されます。JMMは、JVMがローカルメモリとメインメモリ間でデータを転送する必要がある場合のルールを定義します。 Javaアーキテクトは、過度に制限されたメモリモデルはプログラムのパフォーマンスを著しく損なうことに気づきました。最新のコンピュータハードウェアでプログラムを適切に実行できるメモリモデルを作ろうとしましたが、予測可能な方法で相互作用するスレッド。
スレッド間の相互作用を予測可能にレンダリングするためのJavaの主要なツールは、synchronizedキーワードです。多くのプログラマーは、相互排除セマフォ(ミューテックス)を強制して、一度に複数のスレッドによるクリティカルセクションの実行を防止するという意味で、厳密に同期をとっています。残念ながら、その直感は同期の意味を完全には説明していません。
同期のセマンティクスには、セマフォのステータスに基づく実行の相互排除が含まれますが、同期スレッドとメインメモリとの相互作用に関するルールも含まれます。特に、ロックの取得または解放は、メモリバリア(スレッドのローカルメモリとメインメモリ間の強制同期)をトリガーします。 (Alphaなどの一部のプロセッサは、メモリバリアを実行するための明示的な機械命令を備えています。)スレッドが同期ブロックを終了すると、書き込みバリアを実行します。解放する前に、そのブロックで変更された変数をメインメモリにフラッシュする必要があります。ロック。同様に、同期ブロックに入ると、読み取りバリアが実行されます。ローカルメモリが無効化されているかのように、ブロックで参照される変数をメインメモリからフェッチする必要があります。
Javaでダブルチェックロックを正しく行う唯一の方法は、問題の変数に「揮発性」宣言を使用することです。その解決策は正しいですが、「揮発性」はキャッシュラインがフラッシュされることを意味します。すべてのアクセス時。「同期」はブロックの最後でそれらをフラッシュするため、実際には効率的ではない場合があります(または効率がさらに低い場合があります)。コードのプロファイルを作成していない限り、ダブルチェックロックを使用しないことをお勧めしますこの領域にパフォーマンスの問題があることがわかりました。
volatile
midifierでダブルチェックする必要がある変数を定義します
h
変数は必要ありません。これが here の例です
class Foo {
private volatile Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null)
helper = new Helper();
}
}
return helper;
}
}
誰から申告を受け取っているのですか?
Double-Checked Lockingが修正されました。ウィキペディアを確認してください:
public class FinalWrapper<T>
{
public final T value;
public FinalWrapper(T value) { this.value = value; }
}
public class Foo
{
private FinalWrapper<Helper> helperWrapper = null;
public Helper getHelper()
{
FinalWrapper<Helper> wrapper = helperWrapper;
if (wrapper == null)
{
synchronized(this)
{
if (helperWrapper ==null)
helperWrapper = new FinalWrapper<Helper>( new Helper() );
wrapper = helperWrapper;
}
}
return wrapper.value;
}
以下の別の場所からコピーすると、揮発性変数のコピーとしてメソッドローカル変数を使用すると、処理が速くなる理由がわかります。
説明が必要なステートメント:
このコードは少し複雑に見えるかもしれません。特に、ローカル変数の結果の必要性が不明確な場合があります。
説明:
フィールドは、最初のifステートメントで最初に読み取られ、returnステートメントで2回目に読み取られます。フィールドは揮発性として宣言されています。つまり、アクセスされるたびにメモリから再フェッチする必要があり(大まかに言えば、揮発性変数にアクセスするにはさらに多くの処理が必要になる場合があります)、コンパイラーによってレジスターに格納できません。ローカル変数にコピーして両方のステートメント(ifとreturn)で使用すると、JVMでレジスターの最適化を行うことができます。
一部の人が指摘したように、オブジェクトのすべてのメンバーがvolatile
と宣言されていない限り、正しく機能させるにはfinal
キーワードが必要です。デフォルト値を確認できます。
人々がこれを間違えているという絶え間ない問題にうんざりしたので、最終的なセマンティクスを持ち、できるだけ高速になるようにプロファイルおよび調整された LazyReference ユーティリティをコーディングしました。