シングルトン
意図
クラスのインスタンスが 1 つだけであることを確認し、そのインスタンスへのグローバル アクセス ポイントを提供します。
クラス図
プライベート コンストラクター、プライベート スタティック変数、パブリック スタティック関数を使用して実装されます。
プライベート コンストラクターは、コンストラクターを通じてオブジェクト インスタンスを作成できないこと、および唯一のプライベート静的変数はパブリック 静的関数を通じてのみ返されることを保証します。
実装
Ⅰ 遅延スタイル - スレッドは安全ではありません
次の実装では、プライベート静的変数 uniqueInstance が遅延インスタンス化されます。この利点は、クラスが使用されない場合、uniqueInstance がインスタンス化されないため、リソースが節約されることです。
この実装はマルチスレッド環境では安全ではありません。複数のスレッドが同時に入ることができ、if (uniqueInstance == null)
その時点で uniqueInstance が null の場合、複数のスレッドがuniqueInstance = new Singleton();
ステートメントを実行し、その結果 uniqueInstance の複数のインスタンスが作成されます。
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
Ⅱ ハングリーチャイニーズスタイル - スレッドの安全性
スレッドの安全性の低下は、主に uniqueInstance が複数回インスタンス化されることが原因で発生します。uniqueInstance を直接インスタンス化しても、スレッドの安全性が低下することはありません。
ただし、直接インスタンス化方法では、遅延インスタンス化によるリソース節約の利点も失われます。
private static Singleton uniqueInstance = new Singleton();
Ⅲ Lazy スタイル - スレッドの安全性
getUniqueInstance() メソッドのみをロックする必要があるため、一度に 1 つのスレッドのみがメソッドに入ることができるため、uniqueInstance を複数回インスタンス化する必要がなくなります。
ただし、スレッドがこのメソッドに入ると、uniqueInstance がインスタンス化されている場合でも、このメソッドに入ろうとしている他のスレッドは待機する必要があります。これによりスレッドが長時間ブロックされることになるため、この方法にはパフォーマンスの問題があるため、お勧めできません。
public static synchronized Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
Ⅳ ロック糸の安全性を再確認する
uniqueInstance は一度インスタンス化するだけで済み、その後は直接使用できます。ロック操作は、インスタンス化されたコードの一部でのみ実行する必要があり、ロックは uniqueInstance がインスタンス化されていない場合にのみ必要です。
ダブルチェックロックでは、最初に uniqueInstance がインスタンス化されているかどうかが判断され、インスタンス化されていない場合は、インスタンス化ステートメントがロックされます。
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
if ステートメントを 1 つだけ使用する次の実装を考えてみましょう。uniqueInstance == null の場合、両方のスレッドが if ステートメントを実行すると、両方のスレッドが if ステートメント ブロックに入ります。if ステートメント ブロックにはロック操作がありますが、両方のスレッドがこのステートメントを実行しますuniqueInstance = new Singleton();
。これは順序の問題であるため、インスタンス化は 2 回行われます。したがって、二重チェック ロックを使用する必要があります。つまり、2 つの if ステートメントを使用する必要があります。最初の if ステートメントは、uniqueInstance がインスタンス化された後のロック操作を回避するために使用され、2 番目の if ステートメントはロックされているため、1 つのスレッドのみがロックされます。を入力すると、2 つのスレッドが同時にインスタンス化操作を実行する場合、 uniqueInstance == null は存在しません。
if (uniqueInstance == null) {
synchronized (Singleton.class) {
uniqueInstance = new Singleton();
}
}
また、 uniqueInstance を volatile キーワードで変更する必要がありますが、uniqueInstance = new Singleton();
このコードは実際には 3 つのステップで実行されます。
- uniqueInstance にメモリ領域を割り当てる
- uniqueInstance を初期化する
- uniqueInstance が割り当てられたメモリ アドレスを指すようにします。
ただし、JVM の命令並べ替え機能により、実行順序が 1>3>2 になる場合があります。命令の並べ替えは、シングルスレッド環境では問題を引き起こしませんが、マルチスレッド環境では、スレッドがまだ初期化されていないインスタンスを取得する原因となります。たとえば、スレッド T 1 は1 と 3 を実行します。このとき、T 2は getUniqueInstance() を呼び出し、uniqueInstance が空ではないことがわかり、uniqueInstance が返されますが、この時点では uniqueInstance は初期化されていません。
volatile を使用すると、JVM 命令の並べ替えを禁止して、マルチスレッド環境での通常の動作を保証できます。
Ⅴ 静的内部クラスの実装
Singleton クラスがロードされるとき、静的内部クラス SingletonHolder はメモリにロードされません。getUniqueInstance()
SingletonHolder は、INSTANCE インスタンスをトリガーするためにメソッドが呼び出された場合にのみSingletonHolder.INSTANCE
ロードされ、JVM は INSTANCE が 1 回だけインスタンス化されることを保証できます。
このアプローチには、初期化の遅延という利点があるだけでなく、JVM によるスレッド セーフ サポートも提供されます。
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getUniqueInstance() {
return SingletonHolder.INSTANCE;
}
}
Ⅵ 列挙型の実装
public enum Singleton {
INSTANCE;
private String objName;
public String getObjName() {
return objName;
}
public void setObjName(String objName) {
this.objName = objName;
}
public static void main(String[] args) {
// 单例测试
Singleton firstSingleton = Singleton.INSTANCE;
firstSingleton.setObjName("firstName");
System.out.println(firstSingleton.getObjName());
Singleton secondSingleton = Singleton.INSTANCE;
secondSingleton.setObjName("secondName");
System.out.println(firstSingleton.getObjName());
System.out.println(secondSingleton.getObjName());
// 反射获取实例测试
try {
Singleton[] enumConstants = Singleton.class.getEnumConstants();
for (Singleton enumConstant : enumConstants) {
System.out.println(enumConstant.getObjName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
firstName
secondName
secondName
secondName
この実装により、リフレクション攻撃から保護されます。他の実装では、setAccessible() メソッドを通じてプライベート コンストラクターのアクセス レベルをパブリックに設定でき、その後コンストラクターが呼び出されてオブジェクトをインスタンス化します。この攻撃を防止したい場合は、複数のインスタンス化を防止するコンストラクター。この実装は JVM によって 1 回だけインスタンス化されることが保証されているため、前述のリフレクション攻撃は発生しません。
この実装では、シリアル化と逆シリアル化を複数回行った後に複数のインスタンスを取得することはありません。他の実装では、transient を使用してすべてのフィールドを変更し、シリアル化および逆シリアル化メソッドを実装する必要があります。
例
- ロガークラス
- 構成クラス
- 共有モードでのリソースへのアクセス
- シングルトンとして実装されたファクトリー