1. 程式人生 > >設計模式(一):單例模式

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

spa private 軟件設計 是個 shared row 等待 數據庫 獲取鎖

單例模式是一種常用的軟件設計模式,其定義是單例對象的類只能允許一個實例存在。

單例模式一般體現在類聲明中,單例的類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。

適用場合:

  • 需要頻繁的進行創建和銷毀的對象;
  • 創建對象時耗時過多或耗費資源過多,但又經常用到的對象;
  • 工具類對象;
  • 頻繁訪問數據庫或文件的對象。

比如:許多時候整個系統只需要擁有一個的全局對象,這樣有利於我們協調系統整體的行為。比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然後服務進程中的其他對象再通過這個單例對象獲取這些配置信息。這種方式簡化了在復雜環境下的配置管理。

優點:

  • 在內存裏只有一個實例,減少了內存的開銷,尤其是頻繁的創建和銷毀實例(比如網站首頁頁面緩存)。
  • 避免對資源的多重占用(比如寫文件操作)。

二、實現方式

1、普通餓漢式(線程安全,不能延時加載

所謂餓漢。這是個比較形象的比喻。對於一個餓漢來說,他希望他想要用到這個實例的時候就能夠立即拿到,而不需要任何等待時間。

public class Singleton {

    private final static Singleton INSTANCE = new Singleton();

    private Singleton(){}

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

優點:寫法簡單 線程安全

通過static的靜態初始化方式,在該類第一次被加載的時候,就有一個SimpleSingleton的實例被創建出來了。這樣就保證在第一次想要使用該對象時,他已經被初始化好了。

同時,由於該實例在類被加載的時候就創建出來了,所以也避免了線程安全問題。

JVM類加載機制中:

“ 並發:

  虛擬機會保證一個類的類構造器<clinit>()在多線程環境中被正確的加鎖、同步,如果多個線程同時去初始化一個類,那麽只會有一個線程去執行這個類的類構造器<clinit>(),其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。

特別需要註意的是,在這種情形下,其他線程雖然會被阻塞,但如果執行<clinit>()方法的那條線程退出後,其他線程在喚醒之後不會再次進入/執行<clinit>()方法,因為在同一個類加載器下,一個類型只會被初始化一次。

缺點:在類裝載的時候就完成實例化,沒有達到Lazy Loading的效果。

在類被加載的時候對象就會實例化。這也許會造成不必要的消耗,因為有可能這個實例根本就不會被用到。

想象一下,如果實例化instance很消耗資源,我想讓他延遲加載,另外一方面,我不希望在Singleton類加載時就實例化,因為我不能確保Singleton類還可能在其他的地方被主動使用從而被加載,那麽這個時候實例化instance顯然是不合適的。

解決不能Lazy Loading懶加載問題的辦法:第一種是使用靜態內部類的形式。第二種是使用懶漢式。下文會介紹。

2、靜態代碼塊餓漢式(線程安全,不能延時加載

public class Singleton {

    private static Singleton instance;

    static {
        instance = new Singleton();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}

和第一種一樣,只不過將類實例化的過程放在了靜態代碼塊中,也是在類裝載的時候,就執行靜態代碼塊中的代碼,初始化類的實例。

3、靜態內部類(線程安全,延遲加載,效率高

public class Singleton {

    private Singleton() {}

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

    public static Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

加載類 Singleton 時不會實例化對象,加載類 SingletonInstance 時才會實例化對象(也就是調用Singleton的getInstance方法時),實現了延遲加載。

關於類加載機制:JVM類加載機制

優點:線程安全,延遲加載,效率高。

4、枚舉(線程安全,不能延時加載

public enum Singleton {
    INSTANCE;
    public void whateverMethod() {

    }
}

這種方式是Effective Java作者Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象。

由於1.5中才加入enum特性,用這種方式寫不免讓人感覺生疏,在實際工作中,我也很少看見有人這麽寫過,但是不代表他不好。

原理其實也是利用類加載機制實現線程安全。

反編譯後:

public final class Singleton extends Enum<Singleton> {
    public static final Singleton INSTANCE = new Singleton("INSTANCE", 0);
    private static final Singleton[] $VALUES;

    public static Singleton[] values() {
        return (Singleton[])$VALUES.clone();
    }

    public static Singleton valueOf(String string) {
        return Enum.valueOf(Singleton.class, string);
    }

    private Singleton(String string, int n) {
        super(string, n);
    }

    public void whateverMethod() {
    }

    static {
        $VALUES = new Singleton[]{INSTANCE};
    }
}

關於枚舉原理:JDK源碼學習筆記——Enum枚舉使用及原理

優點:簡單 線程安全

缺點:不能延遲加載 使用較少

5、普通懶漢式(線程不安全,可延時加載

public class Singleton {

    private static Singleton singleton;

    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

優點:可以實現延遲加載

缺點:線程不安全

多個線程可能同時進入if 中,創建出多個實例

6、synchronized 懶漢式(線程安全,可延時加載,效率低

public class Singleton {

    private static Singleton singleton;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

優點:可以實現延遲加載,線程安全

缺點:效率低

只有第一次創建實例的時候需要同步,其他情況都不需要。

我們知道synchronized是一個效率比較低的加鎖方式,而每次獲取實例都會同步加鎖(本身不需要同步,直接返回 instance 即可),效率會很低。

7、雙重校驗鎖懶漢式(線程安全,可延時加載,效率高

詳細可參考:Java並發(七):雙重檢驗鎖定DCL   Java並發(二):Java內存模型

對於第六中方法進行優化,減小鎖的粒度:

public class Singleton {
        private static Singleton singleton;
        Integer a;

        private Singleton(){}

        public static Singleton getInstance(){
            if(singleton == null){                              // 1 只有singleton==null時才加鎖,性能好
                synchronized (Singleton.class){                 // 2
                    if(singleton == null){                      // 3
                        singleton = new Singleton();            // 4
                    }
                }
            }
            return singleton;
        }
    }

會因為重排序出現問題:

線程A發現變量沒有被初始化, 然後它獲取鎖並開始變量的初始化。

由於某些編程語言的語義,編譯器生成的代碼允許在線程A執行完變量的初始化之前,更新變量並將其指向部分初始化的對象。

線程B發現共享變量已經被初始化,並返回變量。由於線程B確信變量已被初始化,它沒有獲取鎖。如果在A完成初始化之前共享變量對B可見(這是由於A沒有完成初始化或者因為一些初始化的值還沒有穿過B使用的內存(緩存一致性)),程序很可能會崩潰。

利用volatile限制重排序:

public class Singleton {

    private static volatile Singleton singleton;

    private Singleton() {}

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

三、單例與序列化

1、序列化對單例的破壞

雙重檢驗鎖實現單例:

public class Singleton implements Serializable{
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

測試序列化對單例的影響:

public class SerializableDemo1 {
    //為了便於理解,忽略關閉流操作及刪除文件操作。真正編碼時千萬不要忘記
    //Exception直接拋出
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //Write Obj to file
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
        oos.writeObject(Singleton.getSingleton());
        //Read Obj from file
        File file = new File("tempFile");
        ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
        Singleton newInstance = (Singleton) ois.readObject();
        //判斷是否是同一個對象
        System.out.println(newInstance == Singleton.getSingleton());
    }
}
//false

通過對Singleton的序列化與反序列化得到的對象是一個新的對象,這就破壞了Singleton的單例性。

2、分析

ois.readObject(); 調用的 readOrdinaryObject 方法

private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        //此處省略部分代碼

        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        //此處省略部分代碼

        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }

isInstantiable:如果一個serializable/externalizable的類可以在運行時被實例化,那麽該方法就返回true。針對serializable和externalizable我會在其他文章中介紹。

desc.newInstance:該方法通過反射的方式調用無參構造方法新建一個對象。

hasReadResolveMethod:如果實現了serializable 或者 externalizable接口的類中包含readResolve則返回true

invokeReadResolve:通過反射的方式調用要被反序列化的類的readResolve方法。

原因:序列化會通過反射調用無參數的構造方法創建一個新的對象

解決:在Singleton中定義readResolve方法,並在該方法中指定要返回的對象的生成策略,就可以防止單例被破壞。

3、解決

public class Singleton implements Serializable{
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    privatereturn singleton;
    }
}

總結:一旦實現了Serializable接口之後,就不再是單例的了,因為,每次調用 readObject()方法返回的都是一個新創建出來的對象。解決辦法就是使用readResolve()方法來避免此事發生。

四、關於枚舉實現單例的序列化問題

為了保證枚舉類型像Java規範中所說的那樣,每一個枚舉類型極其定義的枚舉變量在JVM中都是唯一的,在枚舉類型的序列化和反序列化上,Java做了特殊的規定:

在序列化的時候Java僅僅是將枚舉對象的name屬性輸出到結果中,反序列化的時候則是通過java.lang.Enum的valueOf方法來根據名字查找枚舉對象。同時,編譯器是不允許任何對這種序列化機制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

所以,枚舉實現的單例不會有序列化問題

參考資料 / 相關推薦:

Java並發(二):Java內存模型

Java並發(七):雙重檢驗鎖定DCL

JDK源碼學習筆記——Enum枚舉使用及原理

JVM類加載機制

單例模式的八種寫法比較

設計模式(二)——單例模式

深度分析Java的枚舉類型—-枚舉的線程安全性及序列化問題

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