天天看點

設計模式一: 單例模式(Singleton)

簡介

單例模式是屬于建立型模式的一種(另外兩種分别是結構型模式,行為型模式).是設計模式中最為簡單的一種.

英文單詞Singleton的數學含義是"有且僅有一個元素的集合".

從實作層面看, 由類自身管理自己的唯一對象,這個類提供了通路該對象的方式,可以直接通路,不需要執行個體化(使用new).

動機

設計模式中的Singleton的目的是使類的對象在應用程式中保持唯一,這在某些應用場合非常重要,比如檔案系統的資料總管,又比如應用的日志,應用程式配置等. 保持唯一執行個體有利于節約系統資源, 同時提升了應用程式性能,避免對資源的多重占用.

實作

一. 簡單實作(線程不安全)-懶漢模式

public class Singleton{
    private static Singleton onlyOneInstance;

    private Singleton(){
    }

    public static Singleton getOnlyOneInstance(){
        if(onlyOneInstance == null){
            onlyOneInstance = new Singleton();
        }
        return onlyOneInstance;
    }
}           

如果有多個線程同時運作到

if(onlyOneInstance == null)

,并且此時執行個體為null,那多個線程會執行

onlyOneInstance = new Singleton();

語句,導緻執行個體化多次.

二. 簡單實作(線程安全)-懶漢模式

public static synchronized Singleton getOnlyOneInstance(){
    if(onlyOneInstance == null){
        onlyOneInstance = new Singleton();
    }
    return onlyOneInstance;
}           

在方法

getOnlyOneInstance()

上增加同步修飾符

synchronized

,這樣可以保證同一時間隻有一個線程通路方法,進而確定不會發生多次執行個體化.

問題是多個線程通路該方法時會發生阻塞,導緻其他線程會在該方法上等待,是以性能上有損耗.

三. 最簡單實作(線程安全)-餓漢模式

private static Singleton onlyOneInstance = new Singleton();           

這個實作是線程安全的, 但同時也沒有延遲加載帶來的節約資源的好處.

四. 雙重校驗鎖(線程安全)

public class Singleton{

    private volatile static Singleton onlyOneInstance;

    private Singleton(){
    }

    public static Singleton getOnlyOneInstance(){
        if(onlyOneInstance == null){
            synchronized(Singleton.class){
                if(onlyOneInstance == null){
                    onlyOneInstance = new Singleton();
                }
            }
        }
        return onlyOneInstance;
    }
}           

與第二點的代碼比較,本方法将同步鎖放在了方法内部,這樣可以減少部分線程阻塞,因為在第一個判斷

if(onlyOneInstance == null)

時如果執行個體不為null,方法就傳回了.

與第二點的代碼比較,多個線程依然可能會同時進入到

if(onlyOneInstance == null)

,同樣的,此時onlyOneInstance有可能是null,是以需要在

synchronized(Singleton.class)

内部再次判斷

if(onlyOneInstance == null)

onlyOneInstance 使用 volatile 修飾的必須的.解釋這點的原因需要深入到 JVM 的指令重排機制.

onlyOneInstance = new Singleton();

的執行需要分三步:

1. 配置設定記憶體
2. 初始化對象
3. 将 onlyOneInstance 指向配置設定的記憶體位址           

由于 JVM 的指令重排特性,上述三步步驟可能會重排為 1>3>2 ,在多線程環境下通路onlyOneInstance時, 有可能會得到一個未被初始化的執行個體.而valatile關鍵字可以阻止 JVM 的指令重排,進而保證多線程環境下正常運作.

五. 靜态内部類實作

public class Singleton{

    private Singleton(){}

    private static class SingletonHolder{
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getUniqueInstance(){
        return SingletonHolder.INSTANCE;
    }
}           

這種實作利用了 JVM 保持線程安全性, 同時也具備了延遲加載的好處.

六. 枚舉實作

public enum Singleton{
    INSTANCE
}           

單例的最佳實踐, 一則實作簡單, 二則面對複雜的序列化或反射攻擊的時候能夠防止執行個體化多次.

反射攻擊:可以使用反射原理, 通過setAccessible()方法提升構造函數的通路級别為public,然後通過new執行個體化對象.

對于序列化和反序列化,因為每一個枚舉類型和枚舉變量在JVM中都是唯一的,即Java在序列化和反序列化枚舉時做了特殊的規定,枚舉的writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法是被編譯器禁用的,是以也不存在實作序列化接口後調用readObject會破壞單例的問題。

枚舉為什麼是最佳實踐?

首先, 枚舉是class實作的,也就是說它可以有成員變量及成員函數,這是我們可以使用它來實作單例的基礎. 另外,枚舉繼承了Enum類,它不能作為子類繼承其他類,但可以實作接口, 它也不能被其他類繼承, 枚舉編譯後的類會添加final修飾符, 編譯後的代碼如下:

public final class EnumClass extends Enum{

}           

其次,枚舉有且僅有private構造器,這點滿足單例模式的條件.而枚舉值的初始化是在靜态代碼中進行.枚舉實作的單例模式不具備懶加載的作用.