Javaにおける単例モードの優れた実現
5441 ワード
単例モードは初心者が接触する最初のいくつかの設計モードの一つと言わざるを得ません.主にその応用シーンが他のモードよりも分かりやすいからです.私たちは知っています.あなたが全体的に制御するクラス、例えば初期化機能、特定の機能を提供するHelperクラスが必要です.単例モードで実現するのは非常に役に立ちます.2つの非常に良いメリットを提供できるからです
1、メモリ容量の最適化、どこにいても同じオブジェクトの同じインスタンスで、メモリを節約する
2、グローバルな同期化は、同じオブジェクトを使用するため、オブジェクトのいくつかの状態が同期され、つまり不一致状態が回避される
しかし、このシンプルなデザインモデルは見た目は簡単だが、実現は非常に複雑で(初心者は非常にシンプルだと思っているが)、「信頼できる」単例モデルを実現するには頭を悩ませる必要がある.実際の実装コードを見てみよう
実装方法1:
本題に戻って、この実現方式は実際には“怠け者のモード”で、それを軽視しないでください、それはLazy Loadの思想に利用することができて、遅延のロードで、あなたが初めてこの単例のクラスを必要とする時だけ、あなたはやっと呼び出すことができます
このコードは、使用する前にメモリ容量を消費する必要はありません.これは最適化方法ですね.
しかし、この簡単な実現はマルチスレッドになると大きな問題に直面し、スレッドA、Bが実行中であると仮定すると、Aが先に判断し、instanceが空であることを発見し、次の文を実行する準備をしている間に、JVMはプロセッサリソースをスレッドBに割り当て、もう一度判断したが、Bもinstanceが空であることを発見した.Aはまだ次の文を実行していないため、メモリがまだ割り当てられていないので、Bは初期化new操作を実行し、JVMがAを起動したとき、それはまた初期化を行い、このようにあなたのinstanceは2回初期化を行い、複数のinstanceインスタンスが現れ、第1点を満たしていないので、この方法はスレッド安全ではありません.では、どのようにして1つのスレッド安全の単例モードを実現することができますか?
インプリメンテーション2:
実装方法3:
上記の性能がだめである以上、最適化を試してみましょう.スレッドの安全を必要とする状態を同じ同期ロックで保護することができます.これは「同時プログラミング実践」が提供した考え方ではありませんか.では、私はこのように最適化するのは完璧ではありません.スレッドAが同期コードブロックに入ったと仮定します.もう1つのスレッドBもコードブロックに入りたい(nullと判断してtrueを表す)しかしBはブロックされてしまい、Aが初期化を完了してからBが即時に入っても問題はない.もう1回の判断があるからである.1回目の初期化を完了してからfalseと判断すると、当然同期コードブロックにも入らない.この方式は性能と並列の要求を兼ね備えていると言えるが、すでに完璧な感じがするが、実際にはこれはメソッドにもDCL(double check lock)という名前があり、しばらくは良い単例同時書き方とされてきたが、java環境下では非常に深いBUGが隠されている
詳しくは言いませんが、例えばBUGの場所をざっと一例で説明します.
instance=new SingletonC()を実行すると、この操作では、実際に3つのステップを実行し、メモリを割り当てたり、初期化したり、instanceをメモリに向けたりするのが普通の3つのステップですが、この3つのステップJVMが実行するときは乱順に実行することができます.つまり、この3つのステップはどうせしなければならないと思っています.そして、すぐに終わります.順序を逆転しても問題はありません.どうしてそう言うのですか.例えば、あなたが炒め物をあまり気にしないなら、例えば私たちの食堂では、塩を入れてから酢を入れても大丈夫ですよね...
しかし、JVMが第1、第3のステップを先に実行し、まだ第2のステップを実行していないと仮定すると、あなたのAスレッドは強制的に切り替えられます.2番目のスレッドBがコードの1番目の判断に入った(まだ同期コードセグメントに入っていません)、その判断でinstanceはnullではないことがわかりました.Aの3番目のステップが実行されているので、スレッドBは同期コードブロックにも入らず、returnの実行に入りました.このときBで得られたinstanceインスタンスはまだ初期化されていません.つまり、メモリ領域を指しているのか全く分かりませんが、誤認していますこれが初期化完了の例であり,プログラムは制御不能な段階に入り,自然とBUGが来た!私たちの前の例では、塩料理を入れさえすれば食べられると思っていたのに、料理を作る同級生(スレッドA)が先に塩を入れたとしたら、もう一人の同級生が来て(スレッドB)、彼は塩を入れたのを見て、そのまま食べに行ったが、この時の料理は完成せず、問題が発生した.
政府は問題を発見した後、volatileキーワードを新たに追加してこの問題を最適化し、JDK 1.5以降のバージョンでは、instanceの定義にvolatileキーワードを追加し、毎回メインメモリからオブジェクトを読み取ることを保証する役割を果たすだけで、DCLの失効を解決することができますが、同様に、パフォーマンスが消費されるに違いありません.
実装方法4:
私が初めて見たときも見落としていましたが、これが「餓漢」式の書き方で、同時性を保証できるのはなぜですか?クラスがロードされると、JVMは一度だけロードされることを保証します.static変数はインスタンス化されるのではなく、クラスがロードされると作成されることも知っています.そうすると、単一のオブジェクトの一意性が保証されるのではないでしょうか.そう言うのは間違いありませんが、この方法にも欠陥があります.前に述べた遅延ロード(メモリを割り当てるのは初めて使用したときだけで、メモリを割り当てる効果が得られます)このモードにはこの利点はありませんが、この欠陥のもう一つの問題は、単一のモードの初期化にリアルタイムパラメータが必要だと仮定すると、クラスのロード時にインスタンスを初期化するしかないので、自然にこの要求はできませんが、それは私たちのためにもっと良いです.の単一の例のモードは1つの構想を提供しました!
実装方法5:
これは「Java同時プログラミング実戦」が推奨する単例モードの方式であり、私が最も良い方法でもあると思います.最初の同時性の問題はinstanceが静的内部クラスであるため、問題は発生しません.さらにSingletonHolderは内部でしかアクセスできず、getInstanceを初めて呼び出す場合にのみ初期化されるため、遅延ロードの特性を維持し、初期化時のパラメータの使用を自然に行うことができます.
このように多くの実現方式を言って、実はいくつかの実現方式は比較的に冷たいので、みんなは見てみることができて、しかし高い同時の情況の下で、最も基本的な実現だけでは足りなくて、上の5種類の絶えず進化から、最終的に静的な内部類の方式はすでにすべての要求を満たしたはずで、優秀な実現と言える.もちろん、きっともっと効果的な実現方法がたくさん待っています.私はこれらの実現の優劣を理解するのに役立つことを望んでいます.
1、メモリ容量の最適化、どこにいても同じオブジェクトの同じインスタンスで、メモリを節約する
2、グローバルな同期化は、同じオブジェクトを使用するため、オブジェクトのいくつかの状態が同期され、つまり不一致状態が回避される
しかし、このシンプルなデザインモデルは見た目は簡単だが、実現は非常に複雑で(初心者は非常にシンプルだと思っているが)、「信頼できる」単例モデルを実現するには頭を悩ませる必要がある.実際の実装コードを見てみよう
実装方法1:
public class SingletonA {
/**
*
*/
private static SingletonA instance = null;
public static SingletonA getInstance() {
if (instance == null) {
instance = new SingletonA();
}
return instance;
}
}
これが最も簡単な実現方法であり、私たちが最初に接触したものでもあります.大学1年生にとって、これはもうすごいことを知っています.本題に戻って、この実現方式は実際には“怠け者のモード”で、それを軽視しないでください、それはLazy Loadの思想に利用することができて、遅延のロードで、あなたが初めてこの単例のクラスを必要とする時だけ、あなたはやっと呼び出すことができます
instance = new SingletonA();
このコードは、使用する前にメモリ容量を消費する必要はありません.これは最適化方法ですね.
しかし、この簡単な実現はマルチスレッドになると大きな問題に直面し、スレッドA、Bが実行中であると仮定すると、Aが先に判断し、instanceが空であることを発見し、次の文を実行する準備をしている間に、JVMはプロセッサリソースをスレッドBに割り当て、もう一度判断したが、Bもinstanceが空であることを発見した.Aはまだ次の文を実行していないため、メモリがまだ割り当てられていないので、Bは初期化new操作を実行し、JVMがAを起動したとき、それはまた初期化を行い、このようにあなたのinstanceは2回初期化を行い、複数のinstanceインスタンスが現れ、第1点を満たしていないので、この方法はスレッド安全ではありません.では、どのようにして1つのスレッド安全の単例モードを実現することができますか?
インプリメンテーション2:
public class SingletonB {
/**
*
*/
private static SingletonB instance = null;
public synchronized static SingletonB getInstance() {
if (instance == null) {
instance = new SingletonB();
}
return instance;
}
}
同期といえばsynchronizedキーワードを忘れたわけがないでしょう.このコードセグメントがスレッドの安全を保証する以上、キーワードでいいです.間違いありませんが、このようなコードには大きなパフォーマンスの問題があります.初めてインスタンスを作成した後、后ろはすべて同期する必要はありません(どうせすべて同じです)、同期ロックに入ることができてロックを解放する时间はとても大きくて、ただ最初の1回の操作のために后ろのすべての性能を放弃して、これは本当に损をしないので、この方式はマルチスレッドの要求に満ちていますが、性能の上の要求は完全に満足していません!実装方法3:
public class SingletonC {
/**
*
*/
private static SingletonC instance = null;
public static SingletonC getInstance() {
if (instance == null) {
synchronized (SingletonC.class) {
if (instance == null) {
instance = new SingletonC();
}
}
}
return instance;
}
}
上記の性能がだめである以上、最適化を試してみましょう.スレッドの安全を必要とする状態を同じ同期ロックで保護することができます.これは「同時プログラミング実践」が提供した考え方ではありませんか.では、私はこのように最適化するのは完璧ではありません.スレッドAが同期コードブロックに入ったと仮定します.もう1つのスレッドBもコードブロックに入りたい(nullと判断してtrueを表す)しかしBはブロックされてしまい、Aが初期化を完了してからBが即時に入っても問題はない.もう1回の判断があるからである.1回目の初期化を完了してからfalseと判断すると、当然同期コードブロックにも入らない.この方式は性能と並列の要求を兼ね備えていると言えるが、すでに完璧な感じがするが、実際にはこれはメソッドにもDCL(double check lock)という名前があり、しばらくは良い単例同時書き方とされてきたが、java環境下では非常に深いBUGが隠されている
詳しくは言いませんが、例えばBUGの場所をざっと一例で説明します.
instance=new SingletonC()を実行すると、この操作では、実際に3つのステップを実行し、メモリを割り当てたり、初期化したり、instanceをメモリに向けたりするのが普通の3つのステップですが、この3つのステップJVMが実行するときは乱順に実行することができます.つまり、この3つのステップはどうせしなければならないと思っています.そして、すぐに終わります.順序を逆転しても問題はありません.どうしてそう言うのですか.例えば、あなたが炒め物をあまり気にしないなら、例えば私たちの食堂では、塩を入れてから酢を入れても大丈夫ですよね...
しかし、JVMが第1、第3のステップを先に実行し、まだ第2のステップを実行していないと仮定すると、あなたのAスレッドは強制的に切り替えられます.2番目のスレッドBがコードの1番目の判断に入った(まだ同期コードセグメントに入っていません)、その判断でinstanceはnullではないことがわかりました.Aの3番目のステップが実行されているので、スレッドBは同期コードブロックにも入らず、returnの実行に入りました.このときBで得られたinstanceインスタンスはまだ初期化されていません.つまり、メモリ領域を指しているのか全く分かりませんが、誤認していますこれが初期化完了の例であり,プログラムは制御不能な段階に入り,自然とBUGが来た!私たちの前の例では、塩料理を入れさえすれば食べられると思っていたのに、料理を作る同級生(スレッドA)が先に塩を入れたとしたら、もう一人の同級生が来て(スレッドB)、彼は塩を入れたのを見て、そのまま食べに行ったが、この時の料理は完成せず、問題が発生した.
政府は問題を発見した後、volatileキーワードを新たに追加してこの問題を最適化し、JDK 1.5以降のバージョンでは、instanceの定義にvolatileキーワードを追加し、毎回メインメモリからオブジェクトを読み取ることを保証する役割を果たすだけで、DCLの失効を解決することができますが、同様に、パフォーマンスが消費されるに違いありません.
実装方法4:
public class SingletonD {
/**
*
*/
private static SingletonD instance = new SingletonD();
public static SingletonD getInstance() {
return instance;
}
}
私が初めて見たときも見落としていましたが、これが「餓漢」式の書き方で、同時性を保証できるのはなぜですか?クラスがロードされると、JVMは一度だけロードされることを保証します.static変数はインスタンス化されるのではなく、クラスがロードされると作成されることも知っています.そうすると、単一のオブジェクトの一意性が保証されるのではないでしょうか.そう言うのは間違いありませんが、この方法にも欠陥があります.前に述べた遅延ロード(メモリを割り当てるのは初めて使用したときだけで、メモリを割り当てる効果が得られます)このモードにはこの利点はありませんが、この欠陥のもう一つの問題は、単一のモードの初期化にリアルタイムパラメータが必要だと仮定すると、クラスのロード時にインスタンスを初期化するしかないので、自然にこの要求はできませんが、それは私たちのためにもっと良いです.の単一の例のモードは1つの構想を提供しました!
実装方法5:
public class SingletonE {
private static class SingletonHolder {
/**
*
*/
static final SingletonE instance = new SingletonE();
}
public static SingletonE getInstance() {
return SingletonHolder.instance;
}
}
これは「Java同時プログラミング実戦」が推奨する単例モードの方式であり、私が最も良い方法でもあると思います.最初の同時性の問題はinstanceが静的内部クラスであるため、問題は発生しません.さらにSingletonHolderは内部でしかアクセスできず、getInstanceを初めて呼び出す場合にのみ初期化されるため、遅延ロードの特性を維持し、初期化時のパラメータの使用を自然に行うことができます.
このように多くの実現方式を言って、実はいくつかの実現方式は比較的に冷たいので、みんなは見てみることができて、しかし高い同時の情況の下で、最も基本的な実現だけでは足りなくて、上の5種類の絶えず進化から、最終的に静的な内部類の方式はすでにすべての要求を満たしたはずで、優秀な実現と言える.もちろん、きっともっと効果的な実現方法がたくさん待っています.私はこれらの実現の優劣を理解するのに役立つことを望んでいます.