java 面試中單例模式基本都是必考的,都有最推薦的方式,也不知道問來幹嘛。下麵記錄一下 ...
java 面試中單例模式基本都是必考的,都有最推薦的方式,也不知道問來幹嘛。下麵記錄一下
餓漢式(也不知道為何叫這個名字)
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
其實真心覺得沒什麼問題
private Singleton
來修飾可以防止創建多個實例- 沒有延遲載入?這是需求不同好嗎!有很多的需求是希望一開始就載入好的,不希望要用的時候再載入的,比如是
java.lang.Runtime
但面試的時候,你不能這樣回答,面試官不開心的,你要回答
- 沒有延遲載入
- 譬如 Singleton 實例的創建是依賴參數或者配置文件的,在 getInstance() 之前必須調用某個方法設置參數給它,是不能使用。(在構造函數上傳參不行嗎?,不考慮延遲載入)
懶漢模式
public class Singleton {
private volatile static Singleton instance; //聲明成 volatile
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
你看加個延遲載入
- 要將 instance 聲明個 volatile 好讓在多線程的環境下可見
- 又要註意在多線程的情況下要主要加鎖,因為會出現一個線程認為是空要構造對象,而另一個對象也認為是空要構造對象是情況
麻煩!!!
但也有應用場景的,在資源占用很多,又不常用的情況下,可以考慮用懶漢模式
懶漢式二式 —— 內部靜態類
public class Singleton {
private final static class SingleHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingleHolder.INSTANCE;
}
}
因為這個是 JVM 保證其線性安全性的,而且會在載入的時候創建,又不依賴版本,所以以前會比較推薦。
致命的危險
其實上面的東東都有致命的危險!反射
上面按正常人類的做法是不會產生多個實例,如果會產生多個實例是說明上面的方式或多或少不夠完美 ,比如反射
@Test
public void test() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Constructor<?> cls = Singleton.class.getDeclaredConstructor();
cls.setAccessible(true);
Singleton singleton = (Singleton) cls.newInstance();
assertNotEquals(singleton,Singleton.getInstance());//明顯這兩個對象是不一樣的!!!
}
也就是說上面的方式面對複雜的序列化或者反射攻擊,可能會出現問題!(當然要先實現 Serializable 介面)
比如:
@Test
public void test() throws ClassNotFoundException, IOException {
//序列化對象到文件
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("singleton"));
objectOutputStream.writeObject(Singleton.getInstance());
//從文件中讀取對象
File file = new File("singleton");
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
Singleton singleton = (Singleton) objectInputStream.readObject();
//判斷是否是同一個對象
assertNotEquals(singleton,Singleton.getInstance());
}
當然也有應對序列化的方式。
就是在類中添加readResolve
函數,因為反序列化中,如果存在這個函數,序列化的結果就是這函數的值。
所以你在要序列化中的類添加這句就可以了。
private Object readResolve() {
return getInstance();
}
但面對反射就確實無力了。
最好的方式 - 枚舉類
public enum SingletonEnum {
INSTANCE;
}
- 面對反射:jvm 直接禁止了通過反射構造枚舉實例的行為!
- 面對序列化:jvm 對 enum 類的序列化,不是調用 writeObject、readObject 這些方法的。結果是就算是序列化後再反序列化,結果都是一樣。
以上