怒我直言,你可能沒那麼瞭解單例模式

 

日頭沒有辜負咱們,咱們也切莫辜負日頭。——沈從文前端

代碼世界中也存在如下順口溜:java

我單身,我驕傲,我爲國家省套套。
我單身,我自豪,我爲祖國省橡膠。

單例模式雖然簡單,但真正懂的內行的人並很少,今天挑戰全網最全的經典設計模式之單例模式。後端

1. 單例模式

定義設計模式

確保一個類在任何狀況下都絕對只有一個實例,並提供一個全局訪問點。
隱藏其構造方法
屬於建立型設計模式緩存

適用場景安全

確保任何狀況下都絕對只有一個實例
ServletContext、ServletConfig、ApplicationContext、DBPoolide

2. 餓漢式單例

定義性能

系統初始化的時候就加載,無論有沒有用到這個單例。優化

優勢ui

執行效率高,性能高,沒有任何的鎖

缺點

某些狀況下,可能會形成內存浪費
可以被反射破壞

代碼

public class HungrySingleton {

    private static final HungrySingleton singleton = new HungrySingleton();

    private HungrySingleton(){}

    public static HungrySingleton getInstance() {
        return singleton;
    }
}

3. 懶漢式單例

定義

系統初始化的時候不建立實例,只有用到的時候才建立實例。

優勢

節省了內存

缺點

synchronized形成性能低下
可以被反射破壞

3.1 方法加鎖寫法

代碼

public class LazySingleton {

    private static LazySingleton singleton = null;

    private LazySingleton(){}


    /**
     * 版本1
     * @return
     */
    private synchronized LazySingleton getInstance() {
        if (null == singleton) {
            singleton = new LazySingleton();
        }
        return singleton;
    }
}

3.2 代碼塊加鎖寫法

代碼

public class LazySingleton {

    private static LazySingleton singleton = null;

    private LazySingleton(){}
    
    /**
     * 版本2 相比版本1優化一點點
     * @return
     */
    private  LazySingleton getInstance() {
        synchronized (LazySingleton.class) {
            if (null == singleton) {
                singleton = new LazySingleton();
            }
        }
        return singleton;
    } 
}

3.3 雙重判斷加鎖寫法

陷阱案例

public class LazySingleton {

    private static LazySingleton singleton = null;

    private LazySingleton(){}
    
    /**
     * 版本3 雙重判斷
     * @return
     */
    private  LazySingleton getInstance() {
        if (null == singleton) {
            synchronized (LazySingleton.class) {
                if (null == singleton) {
                    singleton = new LazySingleton();
                }
            }
        }
        return singleton;
    }
}

版本3看起來相比版本2優化了很多,但其實這種雙重判斷在生產環境有一個極大的漏洞陷阱,就是指令重排序,有須要瞭解的能夠在評論區留言。解決方案也很簡單,就是 volatile 關鍵字。它能夠限制指令重排序。

正確寫法

public class LazySingleton {

    private volatile static LazySingleton singleton = null;

    private LazySingleton(){}
    
    /**
     * 版本3 雙重判斷
     * @return
     */
    private  LazySingleton getInstance() {
        if (null == singleton) {
            synchronized (LazySingleton.class) {
                if (null == singleton) {
                    singleton = new LazySingleton();
                }
            }
        }
        return singleton;
    }
}

雙重判斷的優勢:性能高了,線程安全了。
缺點:代碼可讀性極差,不夠優雅。

3.4 靜態內部類寫法

利用JVM加載類的順序,靜態內部類,只有用到的時候外部類用到靜態內部類的時候纔會加載。

優勢

寫法優雅,利用了Java的語法特色,性能高,避免了內存浪費

缺點

可以被反射破壞

public class LazyStaticInnerSingleton {

    private LazyStaticInnerSingleton(){}

    public static LazyStaticInnerSingleton getInstance() {
        return LazyHolder.INSTANCE;
    }

    private static class LazyHolder {
        private static final LazyStaticInnerSingleton INSTANCE = new LazyStaticInnerSingleton();
    }
}

這種寫法原本應該夠優雅,夠完美,可是卻有一個缺點是能被反射破壞,文章最後我會證實什麼是能被反射破壞。那有沒有寫法能讓這個單例不會被反射破壞?答案是有的!

public class LazyStaticInnerSingleton {

    private LazyStaticInnerSingleton(){
        if (null != LazyHolder.INSTANCE) {
            throw new RuntimeException("不容許非法訪問!");
        }
    }

    public static LazyStaticInnerSingleton getInstance() {
        return LazyHolder.INSTANCE;
    }

    private static class LazyHolder {
        private static final LazyStaticInnerSingleton INSTANCE = new LazyStaticInnerSingleton();
    }
}

這種寫法就解決了被反射破壞的問題。可是看起來不是那麼的優雅。

4. 註冊式單例

定義

將每個實例都緩存到統一的容器中,使用惟一標識獲取實例。

4.1. 枚舉寫法註冊式單例

優勢

寫法優雅,線程安全

缺點

和餓漢式相似,大量使用會形成內存浪費,根本緣由在於枚舉自己的特色。

public enum  EnumSingleton {
    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
}

使用方法

public class Test {

    public static void main(String[] args) {
        EnumSingleton singleton = EnumSingleton.getInstance();
        singleton.setData(new Object());
        singleton.getData();
    }
}

4.2. Spring IOC容器註冊式單例

Spring設計者結合枚舉式單例的寫法和特色,寫了一種本身的IOC 容器註冊式單例。

public class ContainerSingleton {

    private ContainerSingleton() {}

    private static Map<String, Object> ioc = new ConcurrentHashMap<>();

    public Object getInstance(String className) {
        if (!ioc.containsKey(className)) {
            Object instance = null;
            try {
                instance = Class.forName(className).newInstance();
            } catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) {
                e.printStackTrace();
            }
            return instance;
        } else {
            return ioc.get(className);
        }
    }
}

5. ThreadLocal單例

ThreadLocal單例確定會用到ThreadLocal,根據ThreadLocal自己的特色,即同一線程內數據可見,那麼這種單例就有自己的侷限性,使用的不多。我曾經在token登錄的時候用到過。即前端會傳一個token到後端,token能解析出登錄用戶的信息。把解析後的信息放在ThreadLocal中,那麼本次處理請求就能在任何地方獲取登錄用戶信息。

public class ThreadLocalSingleton {
    
    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>(){
        @Override
        protected ThreadLocalSingleton initialValue() {
            return new ThreadLocalSingleton();
        }
    };
    
    private ThreadLocalSingleton() {}
    
    public static ThreadLocalSingleton getInstance() {
        return threadLocalInstance.get();
    }
}

6. 反射破壞單例證實

public class Test1 {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class clazz = HungrySingleton.class;
        Constructor c = clazz.getDeclaredConstructor(null);
        c.setAccessible(true);
        Object o1 = c.newInstance();
        Object o2 = c.newInstance();
        System.out.println(o1 == o2);//會輸出false
    }
}

解決方案就是:構造方法拋異常。

if (null != LazyHolder.INSTANCE) {
    throw new RuntimeException("不容許非法訪問!");
}

7. 高高高手須要知道的-序列化破壞單例

首先你必須知道什麼是序列化。序列化就是JVM內存中的對象,序列化到磁盤文件,再讀取到內存,不一樣進程的數據交互須要序列化才能傳輸。
以上的全部單例模式,解決了各類各樣的問題,但都存在同一個問題,就是都會被序列化破壞。意思就是:系統中的單例,被序列化到磁盤,而後再加載到內存,那麼這序列化先後兩個單例,並非同一個單例。這就是序列化破壞單例。
解決方案:在單例中加入如下方法:

private Object readResolve() {
    // instead of the object we're on,
    // return the class variable INSTANCE
    return INSTANCE;
}

最後

感謝您閱讀本文,若是您以爲文章寫的對您有用的話,請您點擊上面的「關注」,點個贊,亦可關注公衆號《AIO生活》,這樣您就能夠第一時間收到個人最新文章。

文章內容屬於本身的一點點心得,不免有不對的地方,歡迎在下方評論區探討,您的關注是我創做優質文章的動力。

相關文章
相關標籤/搜索