關於java單例模式,這篇已經講得很清楚了,建議收藏! 概念 特點 餓漢式: 懶漢式: 考慮線程安全的寫法: 兼顧線程安全和效率的寫法: 小坑: 靜態內部類法: 登記式: 枚舉寫法: 最後

概念

java中單例模式是一種常見的設計模式,單例模式分三種:懶漢式單例、餓漢式單例、登記式單例三
種。

特點

單例模式有以下特點:

  • 單例類只能有一個實例。
  • 單例類必須自己創建自己的唯一實例。
  • 單例類必須給所有其他對象提供這一實例
    單例模式確保某個類只有一個實例,而且自行實例化並向整個系統提供這個實例。避免生成多個對
    象保證只對這一個唯一對象進行操作,保證線程的安全和數據的安全.

餓漢式:

顧名思義,餓漢式就是在第一次引用該類的時候就創建對象實例,而不管實際是否需要創建。代碼如
下:
下面看一個示例:

public class Singleton { 
    private static Singleton = new Singleton(); 
    private Singleton() {} 
    public static getSignleton(){ return singleton; }
}

這樣做的好處是編寫簡單,但是無法做到延遲創建對象。但是我們很多時候都希望對象可以儘可能地
延遲加載,從而減小負載,所以就需要下面的懶漢式

懶漢式:

單線程寫法
public class Singleton { private static Singleton = new Singleton(); private Singleton() {} public static getSignleton(){ return singleton; }}
這種寫法是最簡單的,由私有構造器和一個公有靜態工廠方法構成,在工廠方法中對singleton進行null
判斷,如果是null就new一個出來,最後返回singleton對象。這種方法可以實現延時加載,但是有一個
致命弱點:線程不安全。如果有兩條線程同時調用getSingleton()方法,就有很大可能導致重複創建對
象。

public class Singleton { 
    private static Singleton singleton = null; 
    private Singleton(){} 
    public static Singleton getSingleton() { 
    if(singleton == null) singleton = new Singleton(); 
    return singleton; 
    }
}

考慮線程安全的寫法:

這種寫法考慮了線程安全,將對singleton的null判斷以及new的部分使用synchronized進行加鎖。同
時,對singleton對象使用volatile關鍵字進行限制,保證其對所有線程的可見性,並且禁止對其進行指
令重排序優化。如此即可從語義上保證這種單例模式寫法是線程安全的。注意,這裏說的是語義上,實
際使用中還是存在小坑的

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

兼顧線程安全和效率的寫法:

雖然上面這種寫法是可以正確運行的,但是其效率低下,還是無法實際應用。因爲每次調用
getSingleton()方法,都必須在synchronized這裏進行排隊,而真正遇到需要new的情況是非常少的。
所以,就誕生了第三種寫法

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

這種寫法被稱爲“雙重檢查鎖”,顧名思義,就是在getSingleton()方法中,進行兩次null檢查。看似多
此一舉,但實際上卻極大提升了併發度,進而提升了性能。爲什麼可以提高併發度呢?就像上文說的,
在單例中new的情況非常少,絕大多數都是可以並行的讀操作。因此在加鎖前多進行一次null檢查就可
以減少絕大多數的加鎖操作,執行效率提高的目的也就達到了

小坑:

那麼,這種寫法是不是絕對安全呢?前面說了,從語義角度來看,並沒有什麼問題。但是其實還是有
坑。說這個坑之前我們要先來看看 volatile 這個關鍵字。其實這個關鍵字有兩層語義。第一層語義相信
大家都比較熟悉,就是可見性。可見性指的是在一個線程中對該變量的修改會馬上由工作內存( Work
Memory )寫回主內存( Main Memory ),所以會馬上反應在其它線程的讀取操作中。順便一提,工
作內存和主內存可以近似理解爲實際電腦中的高速緩存和主存,工作內存是線程獨享的,主存是線程共
享的。 volatile 的第二層語義是禁止指令重排序優化。大家知道我們寫的代碼(尤其是多線程代碼),
由於編譯器優化,在實際執行的時候可能與我們編寫的順序不同。編譯器只保證程序執行結果與源代碼
相同,卻不保證實際指令的順序與源代碼相同。這在單線程看起來沒什麼問題,然而一旦引入多線程,
這種亂序就可能導致嚴重問題。 volatile 關鍵字就可以從語義上解決這個問題。
注意,前面反覆提到 “ 從語義上講是沒有問題的 ” ,但是很不幸,禁止指令重排優化這條語義直到 jdk1.5
以後才能正確工作。此前的 JDK 中即使將變量聲明爲 volatile 也無法完全避免重排序所導致的問題。所
以,在 jdk1.5 版本前,雙重檢查鎖形式的單例模式是無法保證線程安全的。

靜態內部類法:

那麼,有沒有一種延時加載,並且能保證線程安全的簡單寫法呢?我們可以把 Singleton 實例放到一個
靜態內部類中,這樣就避免了靜態實例在 Singleton 類加載的時候就創建對象,並且由於靜態內部類只
會被加載一次,所以這種寫法也是線程安全的:

public class Singleton { 
    private static class Holder { 
        private static Singleton singleton = new Singleton(); 
    }
    private Singleton(){} 
    public static Singleton getSingleton(){ 
        return Holder.singleton; 
    }
}

但是,上面提到的所有實現方式都有兩個共同的缺點:
都需要額外的工作(Serializable、transient、readResolve())來實現序列化,否則每次反序列化一 個序列化的對象實例時都會創建一個新的實例。
可能會有人使用反射強行調用我們的私有構造器(如果要避免這種情況,可以修改構造器,讓它在 創建第二個實例的時候拋異常)。

登記式:

登記式單例實際上維護了一組單例類的實例,將這些實例存放在一個 Map (登記薄)中,對於已經登記
過的實例,則從 Map 直接返回,對於沒有登記的,則先登記,然後返回。

public class Singleton { 
    private static Map<String, Singleton> map = new HashMap<String, Singleton> (); 
    static { 
        Singleton single = new Singleton(); 
        map.put(single.getClass().getName(), single); 
    }
    // 保護的默認構造子 protected Singleton() { }
    // 靜態工廠方法,返還此類惟一的實例 
    public static Singleton getInstance(String name) { 
        if (name == null) { 
            name = Singleton.class.getName(); 
            System.out.println("name == null" + "--->name=" + name); 
        }
        if (map.get(name) == null) { 
            try {
                map.put(name, (Singleton) Class.forName(name).newInstance());
            } catch (InstantiationException e) { 
                e.printStackTrace(); 
            } catch (IllegalAccessException e) { 
                e.printStackTrace(); 
            } catch (ClassNotFoundException e) { 
                e.printStackTrace(); 
            } 
        }
        return map.get(name);
    }
}

它用的比較少,另外其實內部實現還是用的餓漢式單例,因爲其中的 static 方法塊,它的單例在類被裝
載的時候就被實例化了。

枚舉寫法:

當然,還有一種更加優雅的方法來實現單例模式,那就是枚舉寫法

public enum Singleton { 
    INSTANCE; 
    private String name; 
    public String getName(){ return name; }
    public void setName(String name){ this.name = name; }
}

使用枚舉除了線程安全和防止反射強行調用構造器之外,還提供了自動序列化機制,防止反序列化
的時候創建新的對象。因此推薦儘可能地使用枚舉來實現單例。
最後,不管採取何種方案,請時刻牢記單例的三大要點:
線程安全
延遲加載
序列化與反序列化安全

最後

在文章的最後作者爲大家整理了很多資料!包括java核心知識點+全套架構師學習資料和視頻+一線大廠面試寶典+面試簡歷模板+阿里美團網易騰訊小米愛奇藝快手嗶哩嗶哩面試題+Spring源碼合集+Java架構實戰電子書等等!

全部免費分享給大家,有需要的朋友關注公衆號:【前程有光】自取!

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