1. 程式人生 > >java中的設計模式-單例模式

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 這些方法的。結果是就算是序列化後再反序列化,結果都是一樣。

以上