在面試中被問到頻率最高的設計模式是單例,由於它寫起來很簡單,並且瞭解單例模式的都知道,它有餓漢式、懶漢式、DCL(雙重鎖判斷)、靜態內部類以及枚舉等多種寫法。但說實話,在實際應用中,單例用到的並非不少。但做爲設計模式的基本模式之一,咱們也有必要了解單例是否知足需求,例如線程是否安全,是否延遲加載,反射是否安全,序列化是否安全,這是本文重點關注的問題。
單例模式就是在應用的整個生命週期中只存在一個實例。它有不少好處,避免實例對象的重複建立,減小實例對象的重複建立,減小系統開銷。例如spring容器中管理的Bean默認就是單例的。java
public class HungrySingleton implements Serializable{
private static HungrySingleton singleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return singleton;
}
}
複製代碼
之因此implements Serializable(下同),是爲了後面測試序列化是否安全的須要,通常狀況不用加。面試
餓漢式在類加載時期就已經初始化實例,而咱們知道類加載是線程安全的,因此餓漢式是線程安全的。很明顯,它不是延遲加載的,這也是餓漢式的缺點。經過下面的測試方法1,餓漢式不是反射安全的,由於經過反射構造方法產生了兩個實例。經過測試方法2,餓漢式也不是序列化安全的。spring
測試方法1:設計模式
public static void main(String [] args) {
//測試餓漢式反射是否安全
reflectTest();
}
private static void reflectTest() {
HungrySingleton singleton1 = HungrySingleton.getInstance();
HungrySingleton singleton2 = null;
try {
Class<HungrySingleton> clazz = HungrySingleton.class;
Constructor<HungrySingleton> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
singleton2 = constructor.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(singleton1.hashCode());
System.out.println(singleton2.hashCode());
}
複製代碼
運行結果:安全
測試方法2:bash
public static void main(String [] args) {
//測試餓漢式序列化是否安全
serializableTest();
}
private static void serializableTest() {
HungrySingleton singleton1 = HungrySingleton.getInstance();
HungrySingleton singleton2 = null;
try {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(new File("C:\\1.txt")));
outputStream.writeObject(singleton1);
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("C:\\1.txt")));
singleton2 = (HungrySingleton) inputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(singleton1.hashCode());
System.out.println(singleton2.hashCode());
}
複製代碼
運行結果: 多線程
public class LazySingletonThreadNotSafe implements Serializable{
private static LazySingletonThreadNotSafe singleton = null;
private LazySingletonThreadNotSafe() {
}
public static LazySingletonThreadNotSafe getSingleton() {
if (singleton == null) {
singleton = new LazySingletonThreadNotSafe();
}
return singleton;
}
}
複製代碼
懶漢式在餓漢式的基礎上進行了改造,將實例的初始化從類加載過程移到getInstance()方法真正調用時進行。因此具有了延遲加載,但失去了線程安全性。下面的DCL在此基礎上增長了線程安全。從測試方法1和2可知,懶漢式反射不安全,序列化也不安全。 測試方法1:工具
public static void main(String [] args) {
//測試懶漢式反射是否安全
reflectTest();
}
private static void reflectTest() {
LazySingletonThreadNotSafe singleton1 = LazySingletonThreadNotSafe.getSingleton();
LazySingletonThreadNotSafe singleton2 = null;
try {
Class<LazySingletonThreadNotSafe> clazz = LazySingletonThreadNotSafe.class;
Constructor<LazySingletonThreadNotSafe> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
singleton2 = constructor.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(singleton1.hashCode());
System.out.println(singleton2.hashCode());
}
複製代碼
運行結果:測試
測試方法2:ui
public static void main(String [] args) {
//測試懶漢式序列化是否安全
serializableTest();
}
private static void serializableTest() {
LazySingletonThreadNotSafe singleton1 = LazySingletonThreadNotSafe.getSingleton();
LazySingletonThreadNotSafe singleton2 = null;
try {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(new File("C:\\1.txt")));
outputStream.writeObject(singleton1);
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("C:\\1.txt")));
singleton2 = (LazySingletonThreadNotSafe) inputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(singleton1.hashCode());
System.out.println(singleton2.hashCode());
}
複製代碼
運行結果:
public class LazySingletonThreadSafe implements Serializable{
private volatile static LazySingletonThreadSafe singleton =null;
private LazySingletonThreadSafe() {
}
public static LazySingletonThreadSafe getSingleton() {
if (singleton == null) { //1
synchronized (LazySingletonThreadSafe.class) { //2
if (singleton == null) { //3
singleton = new LazySingletonThreadSafe(); //4
}
}
}
return singleton;
}
}
複製代碼
DCL是在懶漢式基礎上的改進,跟懶漢式惟一不一樣的是DCL是線程安全的。你可能會問,有了synchronized保證線程安全,爲啥還要加volatile修飾?由於DCL自己存在一個致命缺陷,就是重排序致使的多線程訪問可能得到一個未初始化的對象。
咱們知道singleton = new LazySingletonThreadSafe();這行代碼在JVM看來有這麼三步:
一、爲對象分配存儲空間
二、初始化對象
三、將singleton引用指向第一步中分配的內存地址
第2步和第3步可能存在重排序。假設線程A按二、3步顛倒的順序執行代碼(發生了重排序),先執行了第3步,此時singleton引用已經指向了第一步中分配的內存地址,當線程B執行getSingleton()方法時,發現singleton != null,就執行得到了尚未初始化的singleton,這樣就出問題了。咱們知道volatile的性質是保證多線程環境下變量的可見性以及禁止指令重排序,因此要加volatile。
public class StaticInnerSingleton implements Serializable{
private StaticInnerSingleton() {
}
/**
* 靜態內部類,它和餓漢式同樣,基於類加載機制的線程安全,又作到延遲加載。
* SingletonHolder是一個內部類,當外部類StaticInnerSingleton被加載的時候不會被加載,
* 調用getSingleton方法的時候纔會被加載。
*/
private static class SingletonHolder {
private static final StaticInnerSingleton singleton = new StaticInnerSingleton();
}
public static StaticInnerSingleton getSingleton() {
return SingletonHolder.singleton;
}
複製代碼
}
靜態內部類和餓漢式同樣是線程安全的,同時又作到了延遲加載。可是反射不安全,序列化也不安全。
測試方法1:
public static void main(String [] args) {
//測試靜態內部類反射是否安全
reflectTest();
}
private static void reflectTest() {
StaticInnerSingleton singleton1 = StaticInnerSingleton.getSingleton();
StaticInnerSingleton singleton2 = null;
try {
Class<StaticInnerSingleton> clazz = StaticInnerSingleton.class;
Constructor<StaticInnerSingleton> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
singleton2 = constructor.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(singleton1.hashCode());
System.out.println(singleton2.hashCode());
}
複製代碼
運行結果:
測試方法2:
public static void main(String [] args) {
//測試靜態內部類序列化是否安全
serializableTest();
}
private static void serializableTest() {
StaticInnerSingleton singleton1 = StaticInnerSingleton.getSingleton();
StaticInnerSingleton singleton2 = null;
try {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(new File("C:\\1.txt")));
outputStream.writeObject(singleton1);
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("C:\\1.txt")));
singleton2 = (StaticInnerSingleton) inputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(singleton1.hashCode());
System.out.println(singleton2.hashCode());
}
複製代碼
運行結果:
public enum EnumInstance implements Serializable{
INSTANCE;
}
複製代碼
用java反編譯工具看看Enum的源碼,跟餓漢式同樣,是在類加載時就初始化了,是線程安全的,因此並非延遲加載的。
public final class EnumSingleton extends Enum {
public static EnumSingleton[] values() {
return (EnumSingleton[])$VALUES.clone();
}
public static EnumSingleton valueOf(String s) {
return (EnumSingleton)Enum.valueOf(test/singleton/EnumSingleton, s);
}
private EnumSingleton(String s, int i) {
super(s, i);
}
public static final EnumSingleton INSTANCE;
private static final EnumSingleton $VALUES[];
static {
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
}
複製代碼
測試方法:
public static void main(String [] args) {
//測試枚舉反射是否安全
reflectTest();
}
private static void reflectTest() {
EnumInstance singleton1 = EnumInstance.INSTANCE;
EnumInstance singleton2 = null;
try {
Class<EnumInstance> clazz = EnumInstance.class;
Constructor<EnumInstance> constructor = clazz.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
singleton2 = constructor.newInstance("test",1);
} catch (Exception e) {
e.printStackTrace();
}
}
複製代碼
運行結果:
直接不讓反射了,說明枚舉是反射安全的。在 constructor.newInstance()源碼中,有這麼幾行,是枚舉類型直接拋異常了。最後枚舉單例也是序列化安全的,能夠本身測試一下。
經過以上測試,瞭解了五種單例模式各有優缺點,沒有說哪一種單例模式最好,只有知足需求的纔是最合適的。