二重チェックロックの起源
Java プログラムでは、一部の高価なオブジェクトの初期化を延期し、そのオブジェクトが実際に使用されるときにのみ初期化する必要がある場合があります。このとき、遅延初期化技術が必要です。
遅延初期化を正しく実装するには、ある程度のスキルが必要です。スキルがないと問題が発生しやすくなります。これについては、以下で 1 つずつ紹介します。
オプション 1
public class UnsafeLazyInit{ private static Instance instance; public static Instance getInstance(){ if (instance == null){ instance = new Instance(); } return instance; } }
2 つのスレッドが getInstance を別々に呼び出す場合、共有変数へのアクセスが同期されていないため、次の 2 つの状況が発生する可能性があります:
1.インスタンスが初期化されていないことがわかったので、個別に初期化しました。
2.instance=new インスタンス操作の順序が変更されます。実際の実行プロセスは、最初にメモリを割り当て、次にインスタンスに割り当て、最後に初期化を実行します。この場合、他のスレッドがまだ初期化されていないインスタンス オブジェクトを読み取る可能性があります。
オプション 2
public class UnsafeLazyInit{ private static Instance instance; public static synchronized Instance getInstance(){ if (instance == null){ instance = new Instance(); } return instance; } }
このアプローチの問題は明らかで、インスタンスを読み取るたびに同期する必要があり、パフォーマンスに大きな影響を与える可能性があります。
オプション 3
オプション 3 は間違った二重検出ロックの実装です。コードを見てください:
public class UnsafeLazyInit{ private static Instance instance; public static Instance getInstance(){ if (instance == null){ synchronized(UnsafeLazyInit.classs){ if (instance == null){ instance = new Instance(); } } } return instance; } }
この解決策は、上記の 2 つの解決策の問題を解決しているように見えますが、問題もあります。
問題の根本
instance = new Instance();
実際の実行では、このステートメントは次のように 3 つのステートメントに分割される可能性があります:
memory = allocate(); ctorInstance(memory); //2 instance = memory; //3
並べ替えルールによると、最後の 2 つのステートメントにはデータがありません依存関係があるため、並べ替えが可能です。
並べ替え後は、インスタンス フィールドが割り当てられた後、指すオブジェクトがまだ初期化されていない可能性があり、インスタンス フィールドは他のスレッドによって読み取られる静的フィールドであるため、他のスレッドがインスタンスを読み取ることができることを意味します。ドメインはまだ初期化されていません。
Volatile ベースのソリューション
この問題を解決するには、ステートメント 2 とステートメント 3 の並べ替えを禁止するだけで済みます。これにより、volatile を使用してインスタンスを変更できるようになります。
private volatile static Instance インスタンス;
volatile セマンティクスにより、コンパイラーが volatile 書き込み前から volatile 書き込み後への操作の順序を変更することが禁止されるためです。
クラス初期化に基づく解決策
Java 言語仕様では、各クラスまたはインターフェイス C に対して、それに対応する一意の初期化ロック LC が存在し、C から LC へのマッピングは JVM によって実装されると規定されています。各スレッドがクラスに関する情報を読み取るとき、クラスが初期化されていない場合は、LC を取得して初期化しようとしますが、取得に失敗した場合は、他のスレッドが LC を解放するのを待ちます。 LC が取得できた場合は、クラスの初期化ステータスを確認する必要があります。ビット初期化の場合は、初期化を実行する必要があります。初期化中の場合は、他のスレッドが初期化を完了するまで待つ必要があります。初期化されている場合は、このタイプのオブジェクトを直接使用してください。
public class InstanceFactory{ private static class InstanceHolder{ public static Instance = new Instance(); } public static Instance getInstance(){ return InstanceHolder.instance; //这里将导致instance类被初始化 } }
結論
フィールドの遅延初期化は、クラスの初期化またはインスタンスの作成のオーバーヘッドを削減しますが、遅延初期化フィールドへのゼロアクセスのオーバーヘッドは増加します。ほとんどの場合、通常の初期化は遅延初期化よりも優れています。インスタンス フィールドに対してスレッド セーフな遅延初期化を使用する必要がある場合は、上記の volatile ベースの遅延初期化スキームを使用してください。静的フィールドに対してスレッド セーフな遅延初期化を使用する必要がある場合は、上記のクラス初期化スキーム。