1. 程式人生 > >設計模式(一):單例模式 JVM類載入機制 JDK原始碼學習筆記——Enum列舉使用及原理 Java併發(七):雙重檢驗鎖定DCL Java併發(二):Java記憶體模型 Java併發(二):Java記憶體模型 Java併發(七):雙重檢驗鎖定DCL JDK原始碼學習筆記——Enum列舉使用及原理

設計模式(一):單例模式 JVM類載入機制 JDK原始碼學習筆記——Enum列舉使用及原理 Java併發(七):雙重檢驗鎖定DCL Java併發(二):Java記憶體模型 Java併發(二):Java記憶體模型 Java併發(七):雙重檢驗鎖定DCL JDK原始碼學習筆記——Enum列舉使用及原理

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

單例模式一般體現在類宣告中,單例的類負責建立自己的物件,同時確保只有單個物件被建立。這個類提供了一種訪問其唯一的物件的方式,可以直接訪問,不需要例項化該類的物件。

適用場合:

  • 需要頻繁的進行建立和銷燬的物件;
  • 建立物件時耗時過多或耗費資源過多,但又經常用到的物件;
  • 工具類物件;
  • 頻繁訪問資料庫或檔案的物件。

比如:許多時候整個系統只需要擁有一個的全域性物件,這樣有利於我們協調系統整體的行為。比如在某個伺服器程式中,該伺服器的配置資訊存放在一個檔案中,這些配置資料由一個單例物件統一讀取,然後服務程序中的其他物件再通過這個單例物件獲取這些配置資訊。這種方式簡化了在複雜環境下的配置管理。

優點:

  • 在記憶體裡只有一個例項,減少了記憶體的開銷,尤其是頻繁的建立和銷燬例項(比如網站首頁頁面快取)。
  • 避免對資源的多重佔用(比如寫檔案操作)。

二、實現方式

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的列舉型別—-列舉的執行緒安全性及序列化問題