設計模式詳解--單例模式

設計模式詳解--單例模式

指確保一個類在任何情況下都絕對只有一個實例,並提供一個全局訪問點

如ServletContext、ServletConfig、ApplicationContext、DBPool

隱藏其所有的構造方法,屬於創建型模式

上面給出的定義是十分理想化的單例模式,也是單例模式的最終目標。但是實際開發中往往會遇到各種問題從而實現僞“單例”

也會遇到各種情況導致單例模式被破壞。

創建單例模式的方法常見的有四種,分別是餓漢式、懶漢式、註冊式、ThreadLocal單例,下面我將一一介紹

單例模式常見寫法:

一、餓漢式單例

所謂的餓漢式單例,即在類首次加載時就創建實例(類的加載初始化等問題請查看相關文章,這裏不做過於深入的探討)。給人的感覺像是餓了許久人一看到食物就迫不及待的撲上去大快朵頤一般,因此而得名。

優點:絕對線程安全,在線程還沒出現以前就實例化了,不可能存在訪問安全問題

缺點:有可能浪費內存空間,因爲有可能用不到也初始化了

餓漢式單例也有多種實現方式

1.

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

    private HungrySingleton(){}

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

在創建實例的時候可以看到語句中用到了static  final修飾詞,這兩個沒有一個是沒用的

static自不必說靜態變量被所有的對象所共享,在內存中只有一個副本,它當且僅當在類初次加載時會被初始化。,這是單例的保障。那麼final呢?我看網上有的加了這個關鍵字有的沒加有的加了也是不明覺厲。好奇之下看了下。

fina是從cpu角度考慮保證了單例模式的準確性。

CPU處理通過緩存降低延遲,但是由於CPU主頻超過訪問cache時,會產生cache wait。從而造成性能瓶頸。針對這種情況,多種架構採用了一種將對指令重新排序的功能。對應於創建單例模式中

final的作用即:

1)在構造函數內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用

變量,這兩個操作之間不能重排序

2)初次讀一個包含final域的對象的引用,與隨後初次讀這個final域,這兩個操作之間不能重排序

保證對象的安全發佈,防止對象引用被其他線程在對象被完全構造完成前拿到並使用

要分析上面例子中存在的問題,就要從instance = new Singleton()這句開始,對java來說,創建新的對象並不是一個原子操作,這個過程分成了3步:

1,給 instance 分配內存

2,調用 Singleton 的構造函數來初始化成員變量

3,將instance對象指向分配的內存空間(執行完這步 instance 就爲非 null 了)

關鍵:
1,在JVM的即時編譯器中,存在一個設定,叫做指令重排序。

2,在上面的例子中,2操作依賴1操作,但3操作並不依賴2操作,也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是1-2-3 也可能是1-3-2。如果是後者,則在3執行完畢,2未執行之前,被線程二搶佔了,這時instance已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然後使用,然後順理成章地報錯。

3,JDK1.5以後,因爲內存模型的優化,上面的例子不會再因爲指令重排序而出現問題。

(參考資料 https://blog.csdn.net/lkforce/article/details/70332129 從單例模式挖到內存模型(二)----指令重排序)

但是jdk1.5後修復了也就不存在這個問題了,所以final可加可不加。


2.餓漢式靜態塊單例

 

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

和第一種沒什麼區別。

二、懶漢式單例

被外部類調用時才創建實例,給人一種懶洋洋的感覺,你調用我我才創建,你不吱聲我就不創建。

缺點:有可能造成線程不安全導致創建不止一個實例。原因有可能多個線程同時進入非空判斷爲

true,所以創建多個。

1.

public class LazySimpleSingleton {
    private LazySimpleSingleton(){}
    //靜態塊,公共內存區域
    private static LazySimpleSingleton lazy = null;
    public  static LazySimpleSingleton getInstance(){
        if(lazy == null){
            lazy = new LazySimpleSingleton();
        }
        return lazy;
    }
}

這是最簡單的懶漢式創建方式,但是有可能有線程安全問題,即1線程在執行if(lazy==null)後,失去了cpu的佔有,2線程完成了對象的實例化,這時候CPU時間片給到線程1,線程1繼續執行並覆蓋原有的對象(把引用指向新的對象)。更誇張一點有可能線程2創建完了實例後去用這個實例去執行一些操作然後線程1才覆蓋,就形成了線程不安全的問題。另外,就算沒等到線程2去執行其他操作就被線程1覆蓋,從本質上講也破壞的單例的唯一性。而且所謂的覆蓋也只是把引用指向新的對象,原有的對象並不會馬上刪除。

那麼如何解決這個問題呢,加上個synchronized關鍵字鎖住方法即可

代碼

ublic class LazySimpleSingleton {
    private LazySimpleSingleton(){}
    //靜態塊,公共內存區域
    private static LazySimpleSingleton lazy = null;
    public synchronized static LazySimpleSingleton getInstance(){
        if(lazy == null){
            lazy = new LazySimpleSingleton();
        }
        return lazy;
    }
}

2.

線程安全還能保證是單例,那麼這就是最完美的單例模式了嗎?答案顯然是否定的,這種單例模式雖然是線程安全的,但是因爲加上了synchronized關鍵字會使得性能下降。具體下降多少就需要看併發量了。比如同時100個線程併發的訪問這個實例getInstance,那麼99個就會被阻塞了。

完全規避synchronized是不現實的,聰明的前輩們想出了一個雙重鎖的懶漢式單例來減小鎖競爭機率

代碼如下:

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazy = null;

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

乍一看就是多加了次if條件判斷並且把鎖加在兩次判斷中間。但是這樣就如何減小鎖競爭機率了呢?其實我們在後期調用實例的時候很多情況下都是已經實例化好了的,但是第一種情況還需要加鎖來判斷下,實際上第一種情況中討論的併發交替生成對象概率本來就不高,那麼就判斷等於null的時候加鎖判斷下就好了,如果lazy!=null那麼一定不會有出現多次賦值的情況。

這樣100個併發可能也就10個線程鎖競爭一下,大大減小了鎖競爭的概率。

3.靜態內部類懶漢式單例模式

這種形式兼顧餓漢式的內存浪費,也兼顧synchronized性能問題。完美地屏蔽了這兩個缺點。

代碼

public class LazyInnerClassSingleton {
   
    //每一個關鍵字都不是多餘的
    //static 是爲了使單例的空間共享
    //保證這個方法不會被重寫,重載
    public static final LazyInnerClassSingleton getInstance(){
        //在返回結果以前,一定會先加載內部類
        return LazyHolder.LAZY;
    }

    //默認不加載
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

 

這裏利用了靜態內部類可以被外部類通過類名.方法調用的語法規則,從而實現了創建對象的語句一氣呵成不存在安全問題又可以調用時再加載。但是這種方式也是可以被反射破壞的。

測試反射破壞單例代碼如下:

public class LazyInnerClassSingletonTest {

    public static void main(String[] args) {
        try{
            //很無聊的情況下,進行破壞
            Class<?> clazz = LazyInnerClassSingleton.class;

            //通過反射拿到私有的構造方法
            Constructor c = clazz.getDeclaredConstructor(null);
            //強制訪問,強吻,不願意也要吻
            c.setAccessible(true);

            //暴力初始化
            Object o1 = c.newInstance();

            //調用了兩次構造方法,相當於new了兩次
            //犯了原則性問題,
            Object o2 = c.newInstance();

            System.out.println(o1 == o2);
//            Object o2 = c.newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

運行結果:

說明創建了兩個實例

解決辦法

反射創建對象是調用在構造器裏進行判斷,如果已有實例了就拒絕再創建。

代碼如下

public class LazyInnerClassSingleton {
    //默認使用LazyInnerClassGeneral的時候,會先初始化內部類
    //如果沒使用的話,內部類是不加載的
    private LazyInnerClassSingleton(){
        if(LazyHolder.LAZY != null){
            throw new RuntimeException("不允許創建多個實例");
        }
    }

    //每一個關鍵字都不是多餘的
    //static 是爲了使單例的空間共享
    //保證這個方法不會被重寫,重載
    public static final LazyInnerClassSingleton getInstance(){
        //在返回結果以前,一定會先加載內部類
        return LazyHolder.LAZY;
    }

    //默認不加載
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

再次運行測試類,結果如下:

 

 

也許有人會問,萬一對象是先序列化創建出來的,不是正常創建出來的怎麼辦。其實是無所謂的,只要保證內存中有一個實例就可以了。

既然說到了破壞單例的方法,那麼就再說一個

通過序列化的方式破壞單例

此方法不侷限於懶漢式還是餓漢式,所以此處就拿餓漢式舉例

代碼:

//反序列化時導致單例破壞
public class SeriableSingleton implements Serializable {

    //序列化就是說把內存中的狀態通過轉換成字節碼的形式
    //從而轉換一個IO流,寫入到其他地方(可以是磁盤、網絡IO)
    //內存中狀態給永久保存下來了

    //反序列化
    //將已經持久化的字節碼內容,轉換爲IO流
    //通過IO流的讀取,進而將讀取的內容轉換爲Java對象
    //在轉換過程中會重新創建對象new

    public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }

}

 

測試類:

public class SeriableSingletonTest {
    public static void main(String[] args) {

        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();

        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();


            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SeriableSingleton)ois.readObject();
            ois.close();

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

運行結果:

 

 

可以看到兩個對象的內存地址是不一樣的,單例被破壞了

那麼怎麼解決呢?

我看過一位同學大膽的說“不實現serializable就好了”,哈哈是很有道理,但是實際情況下我們往往都需要去實現的,更別提網絡傳輸數據HTTPRestful了。

在本例中,我們可以看到,序列化實例化對象是通過語句

s1 = (SeriableSingleton)ois.readObject();實現的。點進這個方法,層層剝繭,可以找到這麼一段代碼

 

點進readObject0

 

點進readOrdinaryObject

 

發現有個hasReadResloveMethod,而這個方法的說明是這樣的

/**
 * Returns true if represented class is serializable or externalizable and
 * defines a conformant readResolve method.  Otherwise, returns false.
 */

就是說如果依賴的類有readResolve這個方法那麼就返回true,否則返回false

下面的給rep賦值就是調用了readResolve方法,那麼是不是我們在類中加個readResolve方法,再在這個方法中返回已經創建好的實例對象是不是就可以避免單例被破壞了呢,說幹就幹,我們來試一下

代碼如下

//反序列化時導致單例破壞
public class SeriableSingleton implements Serializable {

    //序列化就是說把內存中的狀態通過轉換成字節碼的形式
    //從而轉換一個IO流,寫入到其他地方(可以是磁盤、網絡IO)
    //內存中狀態給永久保存下來了

    //反序列化
    //將已經持久化的字節碼內容,轉換爲IO流
    //通過IO流的讀取,進而將讀取的內容轉換爲Java對象
    //在轉換過程中會重新創建對象new

    public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }

    private  Object readResolve(){
        return  INSTANCE;
    }

}

 

再跑一遍測試用例,發現姐果果然變成了true,守衛單例成功!

有人奇怪這麼個方法時怎麼來的。我覺得可能JDK的設計開發人員就考慮到了這一點特意加上這個方法防止單例被序列化破壞的吧。

懶漢式單例也到此結束

三、註冊式單例

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

 

枚舉式:

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;
    }
}

首先,枚舉式線程安全的。因爲虛擬機在加載枚舉的類的時候,會使用ClassLoader的loadClass方法,而這個方法使用同步代碼塊保證了線程安全)。所以,創建一個enum類型是線程安全的。

用前面提到的序列化和反射方式測試發現全都報錯無法破壞單例。這是爲什麼呢?

我們可以看一下反射方式newInstance源碼

public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        //這裏判斷Modifier.ENUM是不是枚舉修飾符,如果是就拋異常
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

內部使用的餓漢式,可避免序列壞破壞單例從 jdk層面就爲枚舉不被序列化和反射破壞來保駕護航

關於枚舉爲什麼不怕序列化,可以參考這篇文章https://www.cnblogs.com/z00377750/p/9177097.html深度分析Java的枚舉類型—-枚舉的線程安全性及序列化問題

spring中運用的註冊式單例

 

//Spring中的做法,就是用這種註冊式單例,對象方便管理也是懶加載,但是存在線程安全問題
public class ContainerSingleton {
    private ContainerSingleton(){}
    private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
    public static Object getInstance(String className){
        synchronized (ioc) {
            if (!ioc.containsKey(className)) {
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className, obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return obj;
            } else {
                return ioc.get(className);
            }
        }
    }
}

 

四、ThreadLocal單例

這種辦法就是通過利用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();
    }
}

以上就是關於單例模式的全部介紹,歡迎大家留言批評指正

下一篇 設計模式之原型模式詳解

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章