単例モードと性能比較の詳細


単例モードはjavaで広く用いられている設計モードである.単一インスタンスモードの基本原則は、クラスが1つのインスタンスのみを外部に提供し、単一インスタンスオブジェクトが1回だけ初期化されることです.
単例を実現する基本思想は,構造関数の私有化,自己構築の一例,対外暴露のget法である.その書き方は多種多様であるが,単例パターンの餓漢式,怠け者式,二重検出ロック,静的内部類,列挙の5つの書き方を紹介する.スレッドセキュリティ、反射脆弱性、逆シーケンス化脆弱性の3つの面から分析最適化を行います.最後に各書き方の性能をテストします.
一、単例の5つの書き方
餓漢式
public class HungrySingleton {
	private static HungrySingleton instance =new HungrySingleton();
	private HungrySingleton(){}
	public static HungrySingleton getInstance(){
		return instance;
	}
}

餓漢式の書き方は簡単で、クラスロード時にインスタンスを初期化し、構造器を私有化し、getInstanceメソッドを提供してインスタンスを取得します.餓漢式とは、飢えているということで、初期化したばかりでインスタンスが生成され、訪問してもアクセスしなくてもインスタンスが生成されます.遅延ロードは実装されていません.
同じクラスローダで同じクラスをロードすると1回しかロードされないため、餓漢式の単例はスレッドが安全です.同期がないため、呼び出し効率も高い.欠点は、インスタンスを作成するために実装が遅延ロードされていないことです.つまり、実装が必要でない場合です.一般的に単例を実現する必要がある対象はいずれも比較的資源を占有する対象であり,餓漢式の書き方は比較的資源を消費する.
遅延ロードを実現するために、怠け者式の書き方ができました.
怠け者風
public class LazySingleton {
	private static LazySingleton instance;
	private LazySingleton(){}
	public static synchronized LazySingleton getInstance(){
		if(null==instance){
			instance=new LazySingleton();
		}
		return instance;
	}
}

怠け者式とは、インスタンスを作成するのがおっくうで、必要に応じて作成することです.私有化コンストラクタの基本思想は同じであり,怠け者式単例は構築インスタンスがgetInstanceメソッドを呼び出すときに遅延ロードを実現する.synchronizedキーワードによるスレッドセキュリティ.
怠け者はgetInstanceメソッド全体を同期し、一意のインスタンスが作成されるかどうかにかかわらず同期し、呼び出し効率は自然に低くなります.この問題を最適化するために,二重検出ロック(double check lock)の書き方がある.
ダブルチェックロック
public class DCLSingleton {
	private static DCLSingleton instance;
	private DCLSingleton(){}
	public static DCLSingleton getInstance(){
		if(null==instance){
			synchronized (DCLSingleton.class) {
				if(null==instance){
						instance=new DCLSingleton();
					}
				}
			}
		return instance;
	}
}

二重検出ロックの書き方には、インスタンスが作成されたかどうかの検出が2つあります.怠け者式のメソッド全体の同期をキャンセルします.インスタンスが作成された場合、インスタンスは直接返され、同期コードには入りません.そうしないと、ロックがかかってインスタンスが作成されます.
二重検出ロックはロックの細分化により、スレッドの安全を保証すると同時に効率を向上させる.しかし、コードは複雑です.
静的内部クラス
public class StaticSingleton {
	private static class SingletonClassInstance{
		private static final StaticSingleton instance=new StaticSingleton();
	}
    private StaticSingleton(){}
	public static StaticSingleton getInstance(){
		return SingletonClassInstance.instance;
	}
}

静的内部クラスの書き方は個人的に好きな書き方で、コードは比較的簡単で、クラスのロード時に静的内部クラスを初期化しないので、遅延ロードを実現し、常に1つのインスタンスしかなく、スレッドは安全です.呼び出し効率も比較的高い.
列挙
public enum EnumSingleton {
	//              
	INSTANCE;
}

列挙の要素は天然に単例であり、スレッドは安全で、効率が高く、遅延ロードできない.jdk 1.5は列挙された.
二、単例モードの脆弱性
単一のスキーマの意味は、ユーザが取得したインスタンスが常に同じであることです.通常、スレッドの安全な書き方であれば、この点は保証されます.
しかしjavaでオブジェクトを作成するには、反射と逆シーケンス化で取得したオブジェクトは同じオブジェクトですか?
以下のコードでテストします(餓漢式を例にします):
反射:
//                
	@Test
	public void testReject() throws Exception {
		Class<HungrySingleton> clazz=(Class<HungrySingleton>) Class.forName("com.youzi.singleton.HungrySingleton");
		Constructor<HungrySingleton> c=clazz.getDeclaredConstructor(null);
		c.setAccessible(true);
		HungrySingleton instance1=c.newInstance();
		HungrySingleton instance2=c.newInstance();

		System.out.println("    hashcode:"+instance1.hashCode());
		System.out.println("     hashcode:"+instance2.hashCode());
	}

結果:
元のオブジェクトのhashcode:58042961反射オブジェクトのhashcode:2027961269
逆シーケンス化:
//               
	@Test
	public void testSerialize() throws Exception {
		HungrySingleton instance1= HungrySingleton.getInstance();
		ObjectOutputStream oos= new ObjectOutputStream(new FileOutputStream("D:/temp/ab.txt"));
		oos.writeObject(instance1);
		oos.close();
		ObjectInputStream ois=new ObjectInputStream(new FileInputStream("D:/temp/ab.txt"));
		HungrySingleton instance2=(HungrySingleton) ois.readObject();
		ois.close();
		System.out.println("    hashcode:"+instance1.hashCode());
		System.out.println("       hashcode:"+instance2.hashCode());
	}

注意:逆シーケンス化をテストするには、シーケンス化されたオブジェクトのクラスにSerializableインタフェースを実装する必要があります.
テスト結果:
元のオブジェクトのhashcode:1642360923逆シーケンス化オブジェクトのhashcode:1451270520
試験により,列挙書き方を除いて,他の4つの単例書き方には反射ホールと逆シーケンス化ホールが存在することが分かった.すなわち、この2つの方法で複数のインスタンスを生成することができる.列挙書き方には、反射ホールや逆シーケンス化ホールは天然に存在しません.
この2つの脆弱性について、DCLの書き方に基づいてこの2つの脆弱性の書き方を解決するために、いくつかの最適化を行います.
public class SafeSingleton implements Serializable {
	private static SafeSingleton instance;
	private SafeSingleton(){
		if(instance!=null){
			throw new RuntimeException("           ");
		}
	}
	public static SafeSingleton getInstance(){
		if(null==instance){
			synchronized (SafeSingleton.class) {
				if(null==instance){
					instance=new SafeSingleton();
				}
			}
		}
		return instance;
	}
	
	//              instance
	private Object readResolve(){
		return instance;
	}
}

反射ホールがあるのは、クラスを呼び出すプライベートメソッドを反射することで、プライベートコンストラクタを呼び出すときにインスタンスがすでに存在するかどうかを判断し、存在する場合は例外を投げ出し、新しいインスタンスを作成させないためです.
逆シーケンス化ではreadResolve()メソッドが呼び出されます.このメソッドでインスタンスを直接返すと、逆シーケンス化による新しいインスタンスの生成を防止できます.
三、各書き方の効率をテストする
実は、各書き方に同期があるかどうか、同期粒度によって効率の優劣を判断することができます.ここでは以下のコードでテストして、10個のスレッドを同時に単一の例にアクセスし、各スレッドに100万回アクセスするのにかかる時間を測定します.
public class TestEfficiency {
	public static void main(String[] args) throws Exception {
		long start = System.currentTimeMillis();
		int threadNum=10;
		final CountDownLatch countDownLatch=new CountDownLatch(threadNum);
		
		for(int i=0;i<threadNum;i++){
			new Thread(() -> {
				for (int j = 0; j < 1000000; j++) {
					Object o= HungarySingleton.getInstance();
				}
				countDownLatch.countDown();
			}).start();
		}
		countDownLatch.await();//main    ,       0      
		long end=System.currentTimeMillis();
		System.out.println("   :"+(end-start)+"ms");
	}
}

テスト結果:
単一のタイプ
平均消費時間(ms)
HungrySingleton
98
LazySingleton
484
DCLSingleton
94
StaticSingleton
108
EnumSingleton
97
SafeSingleton
107
怠け者式はインスタンスを取得するたびに同期するため,効率が悪く,その他はそれほど悪くないことがわかる.