二重チェックロックはなぜvolatileフィールドを使用しますか?
二重ロックの由来
一例のモードでは、DCL(二重ロック)の実現方式がある.Javaプログラムでは、オーバーヘッドの高いオブジェクトの初期化操作を遅らせ、これらのオブジェクトを使用する場合にのみ初期化を開始する必要がある場合があります.
次は、非スレッドセキュリティの遅延初期化オブジェクトのインスタンスコードです.
UnsafeLazyInitializationクラスでは,Aスレッドがコード1を実行するとともにBスレッドがコード2を実行すると仮定する.このとき、スレッドAは、instanceリファレンスオブジェクトが初期化を完了していないことを示す可能性があります.
UnsafeLazyInitializationクラスでは、
その後、二重チェックロック(Double-Checked Locking)という「スマート」なテクニックが提案されました.二重チェックロックにより同期のオーバーヘッドを低減したいと考えています.次に、二重チェックロックを使用して遅延初期化を実現するインスタンスコードを示します.
二重チェックロックは完璧に見えますが、これは誤った最適化です.スレッドが1番目に実行され、コードがinstanceがnullでないことに読み込まれた場合、instance参照のオブジェクトが初期化されていない可能性があります.
問題の根源
前の二重チェックロックインスタンスコードの4番目(instance=new Instance();)にオブジェクトが作成されました.この行コードは、次の3行の擬似コードに分解できます.
上の3行の疑似コードのうち2と3の間は、(一部のJITコンパイラでは、このような再ソートが実際に発生している)再ソートされる可能性があります.2と3の間の再ソート後の実行タイミングは、次のとおりです.
マルチスレッド実行シーケンステーブル
時間
スレッドA
スレッドB
T1
A 1:オブジェクトのメモリ領域を割り当てる
T2
A 3:instance指向メモリスペースの設定
T3
B 1:instanceが空かどうかを判断する
T4
B 2:instanceがnullでないため、スレッドBはinstance参照のオブジェクトにアクセスする
T5
A 2:イニシャルオブジェクト
T6
A 4:instance参照のオブジェクトへのアクセス
問題発生の根源を知った後,スレッドセキュリティの遅延初期化を実現するための2つの方法を考えることができる.
1)2と3の並べ替えは許可されていません
2)2と3の並べ替えは許可されますが、他のスレッドがこの並べ替えを「表示」することは許可されません.
後述する2つのソリューションは、それぞれ上記の2つに対応しています.
ソリューション1:volatileベースのソリューション
注意:このソリューションには、JDK 5以降のバージョンが必要です(JDK 5から新しいJSR-133メモリモデル仕様が使用されるため、volatileの意味が強化されます).
宣言オブジェクトの参照がvolatileである場合、3行の擬似コードの2と3の間の並べ替えは、マルチスレッド環境では禁止されます.
ソリューション2:クラス初期化に基づくソリューション
JVMはクラスの初期化フェーズ(つまりクラスがロードされ、スレッドが使用される前)でクラスの初期化を実行します.クラスの初期化を実行する間、JVMはロックを取得する.このロックは、複数のスレッドによる同じクラスの初期化を同期できます.
この特性に基づいて,Initialization On Demand Holder idiomと呼ばれる別のスレッドセキュリティの遅延初期化スキームを実現できる.
フィールド遅延初期化は、初期化クラスまたはインスタンスの作成のオーバーヘッドを低減しますが、遅延初期化されたフィールドへのアクセスのオーバーヘッドを増加させます.通常の初期化は、遅延初期化よりも優れています.インスタンスフィールドにスレッドセキュリティを使用する遅延初期化が必要な場合は、volatileベースの遅延初期化スキームを使用します.静的フィールドにスレッドの安全な遅延初期化を使用するには、上述したクラスベースの初期化スキームを使用します.
一例のモードでは、DCL(二重ロック)の実現方式がある.Javaプログラムでは、オーバーヘッドの高いオブジェクトの初期化操作を遅らせ、これらのオブジェクトを使用する場合にのみ初期化を開始する必要がある場合があります.
次は、非スレッドセキュリティの遅延初期化オブジェクトのインスタンスコードです.
/**
* @author xiaoshu
*/
public class Instance {
}
/**
*
*
* @author xiaoshu
*/
public class UnsafeLazyInitialization {
private static Instance instance;
public static Instance getInstance() {
if (null == instance) {
instance = new Instance();
}
return instance;
}
}
UnsafeLazyInitializationクラスでは,Aスレッドがコード1を実行するとともにBスレッドがコード2を実行すると仮定する.このとき、スレッドAは、instanceリファレンスオブジェクトが初期化を完了していないことを示す可能性があります.
UnsafeLazyInitializationクラスでは、
getInstance()
メソッドを同期処理してスレッドセキュリティの遅延初期化を実現できます.サンプルコードは次のとおりです./**
*
*
* @author xiaoshu
*/
public class SafeLazyInitialization {
private static Instance instance;
public synchronized static Instance getInstance() {
if (null == instance) {
instance = new Instance();
}
return instance;
}
}
getInstance()
メソッドの同期処理によりsynchronizedはパフォーマンスオーバーヘッドを引き起こす.getInstance()メソッドが複数のスレッドで頻繁に呼び出されると、プログラム実行性能が低下します.逆に、getInstance()メソッドが複数のスレッドで頻繁に呼び出されない場合、この遅延初期化スキームは満足できるパフォーマンスを提供することができる.その後、二重チェックロック(Double-Checked Locking)という「スマート」なテクニックが提案されました.二重チェックロックにより同期のオーバーヘッドを低減したいと考えています.次に、二重チェックロックを使用して遅延初期化を実現するインスタンスコードを示します.
/**
*
*
* @author xiaoshu
*/
public class DoubleCheckedLocking {
private static Instance instance;
public static Instance getInstance() {
if (null == instance) { //1.
synchronized (DoubleCheckedLocking.class) { //2.
if (null == instance) { //3:
instance = new Instance(); //4.
}
}
}
return instance;
}
}
二重チェックロックは完璧に見えますが、これは誤った最適化です.スレッドが1番目に実行され、コードがinstanceがnullでないことに読み込まれた場合、instance参照のオブジェクトが初期化されていない可能性があります.
問題の根源
前の二重チェックロックインスタンスコードの4番目(instance=new Instance();)にオブジェクトが作成されました.この行コードは、次の3行の擬似コードに分解できます.
memory = allocate(); //1.
ctorInstance(memory); //2.
instance = memory; //3. instance
上の3行の疑似コードのうち2と3の間は、(一部のJITコンパイラでは、このような再ソートが実際に発生している)再ソートされる可能性があります.2と3の間の再ソート後の実行タイミングは、次のとおりです.
memory = allocate(); //1.
instance = memory; //3. instance
// , !
ctorInstance(memory); //2.
マルチスレッド実行シーケンステーブル
時間
スレッドA
スレッドB
T1
A 1:オブジェクトのメモリ領域を割り当てる
T2
A 3:instance指向メモリスペースの設定
T3
B 1:instanceが空かどうかを判断する
T4
B 2:instanceがnullでないため、スレッドBはinstance参照のオブジェクトにアクセスする
T5
A 2:イニシャルオブジェクト
T6
A 4:instance参照のオブジェクトへのアクセス
問題発生の根源を知った後,スレッドセキュリティの遅延初期化を実現するための2つの方法を考えることができる.
1)2と3の並べ替えは許可されていません
2)2と3の並べ替えは許可されますが、他のスレッドがこの並べ替えを「表示」することは許可されません.
後述する2つのソリューションは、それぞれ上記の2つに対応しています.
ソリューション1:volatileベースのソリューション
/**
*
*
* @author xiaoshu
*/
public class SafeDoubleCheckedLocking {
private volatile static Instance instance;
public static Instance getInstance() {
if (null == instance) {
synchronized (SafeDoubleCheckedLocking.class) {
if (null == instance) {
instance = new Instance();//instance volatile, 。
}
}
}
return instance;
}
}
注意:このソリューションには、JDK 5以降のバージョンが必要です(JDK 5から新しいJSR-133メモリモデル仕様が使用されるため、volatileの意味が強化されます).
宣言オブジェクトの参照がvolatileである場合、3行の擬似コードの2と3の間の並べ替えは、マルチスレッド環境では禁止されます.
ソリューション2:クラス初期化に基づくソリューション
JVMはクラスの初期化フェーズ(つまりクラスがロードされ、スレッドが使用される前)でクラスの初期化を実行します.クラスの初期化を実行する間、JVMはロックを取得する.このロックは、複数のスレッドによる同じクラスの初期化を同期できます.
この特性に基づいて,Initialization On Demand Holder idiomと呼ばれる別のスレッドセキュリティの遅延初期化スキームを実現できる.
/**
*
*
* @author xiaoshu
*/
public class InstanceFactory {
private static class InstanceHolder {
private static Instance instance = new Instance();
}
public static Instance getInstance() {
return InstanceHolder.instance; // InstanceHolder
}
}
フィールド遅延初期化は、初期化クラスまたはインスタンスの作成のオーバーヘッドを低減しますが、遅延初期化されたフィールドへのアクセスのオーバーヘッドを増加させます.通常の初期化は、遅延初期化よりも優れています.インスタンスフィールドにスレッドセキュリティを使用する遅延初期化が必要な場合は、volatileベースの遅延初期化スキームを使用します.静的フィールドにスレッドの安全な遅延初期化を使用するには、上述したクラスベースの初期化スキームを使用します.