單例模式

1、單例模式介紹

一、定義與類型

定義:保證一個類僅有一個實例,並提供一個全局訪問點
類型:建立型java

二、適用場景

想確保任何狀況下都絕對只有一個實例spring

三、優勢

在內存裏只有一個實例,減小了內存開銷
能夠避免對資源的多重佔用
設置全局訪問點,嚴格控制訪問緩存

四、缺點

沒有接口,擴展困難安全

五、重點

私有構造器:禁止從單例類外部構造對象
線程安全
延遲加載:使用時才建立
序列化和反序列化安全:序列化和反序列化會對單例模式進行破壞
反射:防護反射攻擊mybatis

2、代碼示例

一、懶漢式及多線程

注重延遲加載:多線程

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private LazySingleton(){
    }
    public static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

可是存在線程安全問題,因此能夠增長synchronized:ide

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private LazySingleton(){
    }
    public synchronized static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

二、Double Check雙重檢查

可是 synchronized 對性能存在影響,因此可使用Double Check雙重檢查:性能

public class LazyDoubleCheckSingleton {
    private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    private LazyDoubleCheckSingleton(){
    }
    public static LazyDoubleCheckSingleton getInstance(){
        if(lazyDoubleCheckSingleton == null){
            synchronized (LazyDoubleCheckSingleton.class){
                if(lazyDoubleCheckSingleton == null){
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

其中測試

lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();

這一句代碼包含三個步驟:
1.分配內存給這個對象
2.初始化對象
3.設置lazyDoubleCheckSingleton 指向剛分配的內存地址
在java語言規範中 容許在單線程內,不會改變單線程執行結果的重排序。
因此 2和3步可能會存在指令重排序,在單線程中,不會影響執行結果:
this

此時在多線程中:

此時線程1訪問對象,可是對象在線程0中尚未初始化完成,可能就會報異常。
解決方案:
方案一、不容許二、3步驟重排序:
使用volatile關鍵字:

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    private LazyDoubleCheckSingleton(){
    }
    public static LazyDoubleCheckSingleton getInstance(){
        if(lazyDoubleCheckSingleton == null){
            synchronized (LazyDoubleCheckSingleton.class){
                if(lazyDoubleCheckSingleton == null){
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

使用了volatile後,全部線程均可以看到共享內存的最新狀態,保證了內存的可見性。用volatile關鍵字修飾的變量,在進行寫操做時,會多出一些彙編代碼,將當前處理器緩存行的數據寫回到內存,其中涉及到緩存一致性協議。

方案二、容許重排序,但不容許其餘線程看到這個重排序,即靜態內部類

三、靜態內部類

基於類初始化的延遲加載解決方案

public class StaticInnerClassSingleton {
    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }
    public static StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }
    private StaticInnerClassSingleton(){
    }
}

原理:存在Class對象的初始化鎖,而且非構造線程,是看不到指令重排序的。
線程0初始化Class,線程1看不到初始化過程。因此靜態內部類這種方法的核心在於InnerClass這個類的對象初始化鎖

補充:類在如下幾種狀況下被初始化,1.實例被建立(new、反射、序列化),2.靜態方法被調用,3.靜態成員被賦值,4.很是量靜態成員被使用,5.頂級類中有嵌套的斷言語句,6.子類被初始化

四、餓漢式

最簡單的寫法:

public class HungrySingleton {
    private final static HungrySingleton hungrySingleton;
    static{
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

優勢是類加載的時候就完成了初始化,避免了線程同步的問題
缺點是沒有延遲加載的效果,可能形成累成內存浪費
餓漢與懶漢之間最大的區別就是延遲加載:餓漢式很餓,一上來就想吃東西,立刻就把對象建立好了;而懶漢式很是懶,不用它的時候都不會建立這個對象。

五、序列化破壞單例模式

如下序列化和反序列化 將會破壞單例模式:

// 實現序列化接口
public class HungrySingleton implements Serializable {
    private final static HungrySingleton hungrySingleton;
    static{
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

測試類:

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(instance);
        File file = new File("singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        HungrySingleton newInstance = (HungrySingleton) ois.readObject();
        // 將會輸出兩個不一樣的內存地址
        System.out.println(instance);
        System.out.println(newInstance);
    }
}

解決方法:反序列化是經過反射生成對象,在這個過程當中,會判斷是否存在並調用readResolve方法

因此可經過增長readResolve方法防止反序列化:

public class HungrySingleton implements Serializable{
    private final static HungrySingleton hungrySingleton;
    static{
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
    private Object reaResolve(){
        // 返回單例對象
        return hungrySingleton;
    }
}

可是在這個過程當中,仍然被建立了新的對象,只是最後沒有返回而已。

六、反射攻擊

public class Test {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class;
        Constructor<HungrySingleton> declaredConstructor = hungrySingletonClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        HungrySingleton instance = HungrySingleton.getInstance();
        HungrySingleton newInstance = declaredConstructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        // 輸出false
        System.out.println(instance == newInstance);
    }
}

對於餓漢式單例、靜態內部類單例,由於是在類初始化時就建立了對象,因此可在構造器中進行反射防護:

public class HungrySingleton implements Serializable{
    private final static HungrySingleton hungrySingleton;
    static{
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){
        // 反射防護,當類在初始化時,單例就會被初始化,爲第一次調用;反射時,爲第二次調用就會報錯	
        if(hungrySingleton != null){
            throw new RuntimeException("單例構造器禁止反射調用");
        }
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
    private Object readResolve(){
        // 返回單例對象
        return hungrySingleton;
    }
}

而對於不是在類初始化時建立對象的單例模式,則沒法防護反射攻擊,例如懶漢式單例模式:

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private LazySingleton(){
        if(lazySingleton != null){
            throw new RuntimeException("單例構造器禁止反射調用");
        }
    }
    public synchronized static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

由於在被反射攻擊的時候,單例可能尚未被建立,因此會產生不一樣實例,測試類:

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        // 反射攻擊
        Class<LazySingleton> lazySingletonClass = LazySingleton.class;
        Constructor<LazySingleton> declaredConstructor = lazySingletonClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        // 先反射
        LazySingleton newInstance = declaredConstructor.newInstance();
        // 後取單例,由於類中的實例仍爲null,因此構造器的判斷沒有起到想要的做用
        LazySingleton instance = LazySingleton.getInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

能夠增長信號量進行控制:

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private static boolean flag = true;
    private LazySingleton(){
        if (flag){
            flag = false;
        } else {
            throw new RuntimeException("單例構造器禁止反射調用");
        }
    }
    public synchronized static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

可是信號量仍然能夠被修改,以達到反射攻擊:

public class Test {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, NoSuchFieldException, InvocationTargetException {
        Class objectClass = LazySingleton.class;
        Constructor c = objectClass.getDeclaredConstructor();
        c.setAccessible(true);

        LazySingleton o1 = LazySingleton.getInstance();

        Field flag = o1.getClass().getDeclaredField("flag");
        flag.setAccessible(true);
        // 修改信號量
        flag.set(o1,true);

        LazySingleton o2 = (LazySingleton) c.newInstance();

        System.out.println(o1);
        System.out.println(o2);
        // 返回false
        System.out.println(o1==o2);
    }
}

七、Enum枚舉單例

枚舉類型自然的可序列化機制,可以強有力得保證不會屢次實例化的狀況。即便在複雜的序列化或者反射攻擊下,枚舉模式都沒有問題。

public enum EnumInstance {
    INSTANCE{
        protected  void printTest(){
            System.out.println("Geely Print Test");
        }
    };
    protected abstract void printTest();
    private Object data;
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public static EnumInstance getInstance(){
        return INSTANCE;
    }
}

在ObjectInputStream中,對於枚舉類型,是經過枚舉類直接得到惟一的枚舉常量,沒有建立新的對象,維護了枚舉的單例屬性:

而對於反射,在調用
objectClass.getDeclaredConstructor();
時會直接報錯:
java.lang.NoSuchMethodException
緣由在於Enum自己就只有一個構造器:

而若是調用

Constructor constructor = objectClass.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumInstance instance = (EnumInstance) constructor.newInstance("11",22);

也會直接報錯:java.lang.IllegalArgumentException: Cannot reflectively create enum objects

若是經過jad反編譯枚舉類,能夠看到:1.class類爲final的;2.構造器爲private;3.聲明的枚舉對象是static和final的;4.枚舉對象在static代碼塊中實例化
因此枚舉單例是最安全的單例模式

八、容器單例

public class ContainerSingleton {

    private ContainerSingleton(){
    }
    private static Map<String,Object> singletonMap = new HashMap<String,Object>();

    public static void putInstance(String key,Object instance){
        if(StringUtils.isNotBlank(key) && instance != null){
            if(!singletonMap.containsKey(key)){
                singletonMap.put(key,instance);
            }
        }
    }
    public static Object getInstance(String key){
        return singletonMap.get(key);
    }
}

容器單例與享元模式類似
優勢:統一管理,節省資源,至關於緩存
缺點:存在線程安全問題

九、ThreadLocal線程單例

public class ThreadLocalInstance {
    private static final ThreadLocal<ThreadLocalInstance> threadLocalInstanceThreadLocal
             = new ThreadLocal<ThreadLocalInstance>(){
        @Override
        protected ThreadLocalInstance initialValue() {
            return new ThreadLocalInstance();
        }
    };
    private ThreadLocalInstance(){

    }
    public static ThreadLocalInstance getInstance(){
        return threadLocalInstanceThreadLocal.get();
    }
}

這個單例 並不能保證整個應用全局惟一,但能保存線程惟一。
ThreadLocal會爲每個線程提供一個變量副本,自己是基於ThreaLocalMap實現的,維持了線程間的隔離。原理是以空間換時間的方式,會建立不少對象,在一個線程裏會建立惟一的一個對象。在多線程訪問的時候,彼此不會相互影響。

3、源碼示例

一、JDK中的Runtime:餓漢式

二、JDK中的Desktop:懶漢式+容器式+線程安全控制

三、spring

四、mybatis:ThreadLocal

相關文章
相關標籤/搜索