JAvaにおける単例モードの比較
5906 ワード
単例モードはコードが最も少ないモードかもしれませんが、少ないことは必ずしも簡単ではありません.単例モードをよく使って、使うには本当に頭がかかります.本文はJavaの中でよく見られる単例のモードの書き方について1つの総括をして、もし間違いがあるならば、読者に指摘してもらいます.
餓漢法:
実際に作成する必要があるかどうかにかかわらず、クラスを最初に参照するときにオブジェクトインスタンスを作成します.
このような利点は、作成が簡単ですが、オブジェクトの作成を遅らせることはできません.しかし、オブジェクトがロードをできるだけ遅らせ、負荷を減らすことを望んでいる場合が多いので、次の怠け者法が必要です.
シングルスレッドの書き方
この書き方は最も簡単で,私有コンストラクタと公有静的工場法で構成され,工場法ではsingletonをnull判断しnullであればnewが1つ出てきてsingletonオブジェクトに戻る.この方法は遅延ロードを実現できるが,スレッドが安全ではないという致命的な弱点がある.2つのスレッドがgetSingleton()メソッドを同時に呼び出すと、オブジェクトの繰り返し作成につながる可能性があります.
スレッドの安全を考慮した書き方
この書き方はスレッドセキュリティを考慮し,singletonのnull判定およびnewの部分をsynchronizedでロックする.同時にsingletonオブジェクトのvolatileキーワードの使用を制限し、すべてのスレッドの可視性を保証し、命令の再ソート最適化を禁止します.このようにして、このような単一のモードの書き方がスレッドの安全であることを意味的に保証することができる.注意、ここで言うのは意味的に、実際の使用ではまだ小さな穴が存在しており、後述する.
スレッドのセキュリティと効率を両立させた書き方
このような書き方は正しく動作するが,その効率は低下し,実用化は不可能である.getSingleton()メソッドを呼び出すたびにsynchronizedで並ばなければならないため、本当にnewが必要な場合は少ない.3つ目の書き方が生まれました
この書き方は「二重チェックロック」と呼ばれ,その名の通りgetSingleton()メソッドでnullチェックを2回行う.多くのことをしているように見えますが、実際には同時速度が大幅に向上し、パフォーマンスが向上しています.なぜ同時速度を向上させることができるのでしょうか.前述したように、単一の例ではnewの場合は非常に少なく、ほとんどが並列に読み取ることができる.従って,ロック前にnullチェックを1回多く行うことで,ほとんどのロック操作を低減でき,実行効率の向上の目的も達成できる.
では、この書き方は絶対に安全ではないでしょうか.前述したように、意味的には問題ありません.でも実はまだ穴があります.この穴を言う前にvolatileというキーワードを見てみましょう.実はこのキーワードには2つの意味があります.第1層の意味はみんながよく知っていると信じています.可視性です.可視性とは、1つのスレッドで変数の変更がワークメモリ(Work Memory)によってプライマリメモリ(Main Memory)にすぐに書き込まれるため、他のスレッドの読み取り操作にすぐに反応することを意味します.ちなみに、ワークメモリとメインメモリは、実際のコンピュータのキャッシュとメインメモリと近似して理解できます.ワークメモリはスレッド独自で、メインメモリはスレッド共有です.volatileの第2層の意味は,命令の再ソート最適化を禁止することである.私たちが書いたコード(特にマルチスレッドコード)は、コンパイラの最適化によって、実際に実行するときに私たちが書いた順序と異なる可能性があることを知っています.コンパイラは、プログラムの実行結果がソースコードと同じであることを保証するが、実際の命令の順序がソースコードと同じであることを保証しない.これは単一スレッドでは問題ないように見えますが、マルチスレッドが導入されると、この乱順は深刻な問題を引き起こす可能性があります.volatileキーワードは意味的にこの問題を解決することができます.
「意味的には問題ない」と繰り返し述べたが、残念ながらjdk 1.5以降になってから正しく動作するまで、命令の再配置最適化は禁止されている.これまでのJDKでは、変数をvolatileと宣言しても、並べ替えの問題を完全に回避することはできませんでした.したがって、jdk 1.5バージョンまでは、二重チェックロック形式の一例モードではスレッドの安全は保証されません.
静的内部クラス法
では、遅延ロードでスレッドの安全を保証できる簡単な書き方はありますか?Singletonインスタンスを静的内部クラスに配置することで、Singletonクラスのロード時に静的インスタンスがオブジェクトを作成することを回避できます.静的内部クラスは1回しかロードされないため、この書き方もスレッドが安全です.
しかし、上記のすべての実装方法には2つの共通の欠点があります.では、シーケンス化を実現するために追加の作業(Serializable、transient、readResolve()が必要です.そうしないと、シーケンス化されたオブジェクトインスタンスを逆シーケンス化するたびに新しいインスタンスが作成されます. では、反射を使用してプライベートコンストラクタを強制的に呼び出す人がいる可能性があります(これを回避するには、コンストラクタを変更して、2番目のインスタンスを作成するときに異常を放出させることができます).
列挙の書き方
もちろん、単例モードを実現するためのより優雅な方法もあります.それは列挙の書き方です.
列挙を使用すると、スレッドのセキュリティと反射防止によるコンストラクタの強制呼び出しに加えて、逆シーケンス化時に新しいオブジェクトを作成することを防止する自動シーケンス化メカニズムが提供されます.したがって、Effective Javaは、できるだけ列挙を用いて単一の例を実現することを推奨する.
まとめ
この文章を出してから多くのフィードバックを得て、これは私をかわいがって驚いて、もう少し小結を書くべきだと思っています.コードには一労永逸の書き方はなく、特定の条件の下で最も適切な書き方しかありません.異なるプラットフォーム、異なる開発環境(特にjdkバージョン)の下で、自然に異なる最適解(またはより良い解)がある.たとえば、Effective Javaでは推奨されていますが、Androidプラットフォームでは推奨されていません.このAndroid Trainingでは、
Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
例えば二重チェックロック法は、jdk 1.5以前には使用できないが、Androidプラットフォームで使用すると安心できる(一般的にAndroidはjdk 1.6以上であり、volatileの意味の問題を修正するだけでなく、ロックの最適化も多く加わり、マルチスレッド同期のオーバーヘッドが少なくない).
最後に、どのような案を採用しても、単例の3つのポイントを常に覚えておいてください.スレッドセキュリティ 遅延ロード シーケンス化および逆シーケンス化セキュリティ
餓漢法:
実際に作成する必要があるかどうかにかかわらず、クラスを最初に参照するときにオブジェクトインスタンスを作成します.
public class Singleton {
private static Singleton = new Singleton();
private Singleton() {}
public static getSignleton(){
return singleton;
}
}
このような利点は、作成が簡単ですが、オブジェクトの作成を遅らせることはできません.しかし、オブジェクトがロードをできるだけ遅らせ、負荷を減らすことを望んでいる場合が多いので、次の怠け者法が必要です.
シングルスレッドの書き方
この書き方は最も簡単で,私有コンストラクタと公有静的工場法で構成され,工場法ではsingletonをnull判断しnullであればnewが1つ出てきてsingletonオブジェクトに戻る.この方法は遅延ロードを実現できるが,スレッドが安全ではないという致命的な弱点がある.2つのスレッドがgetSingleton()メソッドを同時に呼び出すと、オブジェクトの繰り返し作成につながる可能性があります.
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton() {
if(singleton == null) singleton = new Singleton();
return singleton;
}
}
スレッドの安全を考慮した書き方
この書き方はスレッドセキュリティを考慮し,singletonのnull判定およびnewの部分をsynchronizedでロックする.同時にsingletonオブジェクトのvolatileキーワードの使用を制限し、すべてのスレッドの可視性を保証し、命令の再ソート最適化を禁止します.このようにして、このような単一のモードの書き方がスレッドの安全であることを意味的に保証することができる.注意、ここで言うのは意味的に、実際の使用ではまだ小さな穴が存在しており、後述する.
public class Singleton {
private static volatile Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton(){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
return singleton;
}
}
スレッドのセキュリティと効率を両立させた書き方
このような書き方は正しく動作するが,その効率は低下し,実用化は不可能である.getSingleton()メソッドを呼び出すたびにsynchronizedで並ばなければならないため、本当にnewが必要な場合は少ない.3つ目の書き方が生まれました
public class Singleton {
private static volatile Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
この書き方は「二重チェックロック」と呼ばれ,その名の通りgetSingleton()メソッドでnullチェックを2回行う.多くのことをしているように見えますが、実際には同時速度が大幅に向上し、パフォーマンスが向上しています.なぜ同時速度を向上させることができるのでしょうか.前述したように、単一の例ではnewの場合は非常に少なく、ほとんどが並列に読み取ることができる.従って,ロック前にnullチェックを1回多く行うことで,ほとんどのロック操作を低減でき,実行効率の向上の目的も達成できる.
では、この書き方は絶対に安全ではないでしょうか.前述したように、意味的には問題ありません.でも実はまだ穴があります.この穴を言う前にvolatileというキーワードを見てみましょう.実はこのキーワードには2つの意味があります.第1層の意味はみんながよく知っていると信じています.可視性です.可視性とは、1つのスレッドで変数の変更がワークメモリ(Work Memory)によってプライマリメモリ(Main Memory)にすぐに書き込まれるため、他のスレッドの読み取り操作にすぐに反応することを意味します.ちなみに、ワークメモリとメインメモリは、実際のコンピュータのキャッシュとメインメモリと近似して理解できます.ワークメモリはスレッド独自で、メインメモリはスレッド共有です.volatileの第2層の意味は,命令の再ソート最適化を禁止することである.私たちが書いたコード(特にマルチスレッドコード)は、コンパイラの最適化によって、実際に実行するときに私たちが書いた順序と異なる可能性があることを知っています.コンパイラは、プログラムの実行結果がソースコードと同じであることを保証するが、実際の命令の順序がソースコードと同じであることを保証しない.これは単一スレッドでは問題ないように見えますが、マルチスレッドが導入されると、この乱順は深刻な問題を引き起こす可能性があります.volatileキーワードは意味的にこの問題を解決することができます.
「意味的には問題ない」と繰り返し述べたが、残念ながらjdk 1.5以降になってから正しく動作するまで、命令の再配置最適化は禁止されている.これまでのJDKでは、変数をvolatileと宣言しても、並べ替えの問題を完全に回避することはできませんでした.したがって、jdk 1.5バージョンまでは、二重チェックロック形式の一例モードではスレッドの安全は保証されません.
静的内部クラス法
では、遅延ロードでスレッドの安全を保証できる簡単な書き方はありますか?Singletonインスタンスを静的内部クラスに配置することで、Singletonクラスのロード時に静的インスタンスがオブジェクトを作成することを回避できます.静的内部クラスは1回しかロードされないため、この書き方もスレッドが安全です.
public class Singleton {
private static class Holder {
private static Singleton singleton = new Singleton();
}
private Singleton(){}
public static Singleton getSingleton(){
return Holder.singleton;
}
}
しかし、上記のすべての実装方法には2つの共通の欠点があります.
/** * Singletone 。 * */
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
private static boolean initSign;
private Singleton() {
if (initSign) {
throw new RuntimeException(" ");
}
initSign = true;
}
}
列挙の書き方
もちろん、単例モードを実現するためのより優雅な方法もあります.それは列挙の書き方です.
public enum Singleton {
INSTANCE;
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
列挙を使用すると、スレッドのセキュリティと反射防止によるコンストラクタの強制呼び出しに加えて、逆シーケンス化時に新しいオブジェクトを作成することを防止する自動シーケンス化メカニズムが提供されます.したがって、Effective Javaは、できるだけ列挙を用いて単一の例を実現することを推奨する.
まとめ
この文章を出してから多くのフィードバックを得て、これは私をかわいがって驚いて、もう少し小結を書くべきだと思っています.コードには一労永逸の書き方はなく、特定の条件の下で最も適切な書き方しかありません.異なるプラットフォーム、異なる開発環境(特にjdkバージョン)の下で、自然に異なる最適解(またはより良い解)がある.たとえば、Effective Javaでは推奨されていますが、Androidプラットフォームでは推奨されていません.このAndroid Trainingでは、
Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
例えば二重チェックロック法は、jdk 1.5以前には使用できないが、Androidプラットフォームで使用すると安心できる(一般的にAndroidはjdk 1.6以上であり、volatileの意味の問題を修正するだけでなく、ロックの最適化も多く加わり、マルチスレッド同期のオーバーヘッドが少なくない).
最後に、どのような案を採用しても、単例の3つのポイントを常に覚えておいてください.