1.前言
寫完這個題目,我感覺自己好像"孔乙己"啊,回字的四種寫法要不要學啊~
我們經常會用到單例模式,但是我對他一直沒有一個統一的的認識,比如我清楚好多種單例的寫法,但是每一種是怎麼演化來的?具體解決了什麼問題?這塊就沒有那麼清晰了,因此此文對單例模式進行一個總結,同時手擼一下代碼加深理解.
2.介紹
單例模式,即某一個類在整個系統中有且僅有一個實例.
經常用來讀取配置,獲取連接等等.
3.實現思路
1.構造方法私有化.
2.提供靜態的方法,返回唯一實例.
這塊很好理解,要想保證只有唯一實例,構造方法就不能被別人調用,只能自己調用用來創建唯一的實例,同時,將構造方法私有化了,就需要對外提供一個訪問點,以方便其他類獲取這個實例.
4.具體實現
4.1 餓漢式
這種寫法的優勢就是,真的簡單,基本就是的實現思路的耿直實現,代碼如下:
public class HungrySingleton {
private static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getSingleton() {
return hungrySingleton;
}
}
這樣子有個問題,就是隻要這個類被加載了,那麼就會創建出唯一實例,也不管用不用...
雖然其實工作中問題不大,但是學習嘛,就要吹毛求疵,我們要懶加載的方式!
4.2 懶漢式
代碼如下:
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() {
}
public static LazySingleton getSingleton() {
if (null == lazySingleton) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
這種方式也挺好理解的,而且實現了懶加載!只有在調用的時候才創建實例,節省了好大的空間呢!(並不)
但是這種方式仍然是有問題的,那就是著名的你有現在問題兩個了
.
如果多個線程同時來請求獲取實例,上面這種懶漢式是解決不了的,會提供多個實例,也就違背了單例模式的初衷了(多個線程同時進入判空語句).
4.3 的懶漢優化一下
不就是線程安全嗎?把我知道的volatile和synchronized都用上!
public class LazySingleton2 {
private static volatile LazySingleton2 lazySingleton = null;
private LazySingleton2() {
}
public static LazySingleton2 getSingleton() {
synchronized (LazySingleton2.class) {
if (null == lazySingleton) {
lazySingleton = new LazySingleton2();
}
}
return lazySingleton;
}
}
這種方法看起來沒有問題了,用volatilew修飾了唯一實例,保證內存可見性,用synchronized加鎖,每次只允許一個線程訪問判空語句,這不就解決了上面的問題嗎?
是的,殺雞用牛刀也不一定做的好啊..想想判空語句以及裏面的實例化的執行頻率,從理想的情況來講,只有第一次會執行創建實例,剩下的都是返回實例就完事了.
爲了這一種情況,每次都加鎖,,性能下降太厲害了(其實並不,加了鎖我們大部分時間也是夠用的).
那再優化一下.
4.4 雙重檢查鎖
代碼如下:
public class DoubleCheckSingleton {
private static volatile DoubleCheckSingleton singleton = null;
private DoubleCheckSingleton() {
}
public static DoubleCheckSingleton getSingleton() {
if (null == singleton) {
synchronized (DoubleCheckSingleton.class) {
if (null == singleton) {
singleton = new DoubleCheckSingleton();
}
}
}
//2
return singleton;
}
}
這就是傳說中的雙重檢查鎖
了,說實話,這個代碼看起來我覺得有點難看....
但是其實是比較好使的,大部分的獲取實例請求都會直接來到//2
位置,而極少量的爲空進行加鎖,保證線程安全.
雙重檢查鎖對上一步的優化是:多添加一重判斷,過濾掉大部分不需要加鎖的操作,同時,加鎖後再次進行判斷,防止在第一次判斷-加鎖
期間已經創建了實例.
4.5 靜態內部類實現
public class InnerClassSingleton {
private static class Holder {
private static InnerClassSingleton singleton = new InnerClassSingleton();
}
private InnerClassSingleton() {
}
public static InnerClassSingleton getSingleton() {
return Holder.singleton;
}
}
我們可以把Singleton實例放到一個靜態內部類中,這樣就避免了靜態實例在Singleton類加載的時候就創建對象,並且由於靜態內部類只會被加載一次,所以這種寫法也是線程安全的:
4.6 枚舉寫法
上面的所有實現都有一點小問題:
- 序列化與反序列化沒有考慮,每次反序列化都能拿到一個新的實例.
- 反射,都可以通過反射強行調用privite的構造方法.
這時候就是枚舉類出現的時候了!
public enum EnumSingleton {
SINGLETON;
}
在《Effective Java》最後推薦了這樣一個寫法,看起來簡直簡單的有點不可思議,那麼它是怎麼保證以上幾點的呢?
- 枚舉類的初始化過程天然線程安全.即保證了線程安全.
- 對枚舉的序列化與反序列禁止了自定義,由JDK實現,不會出現反序列化多個實例的情況.
在 《Effctive Java》中,作者極力推薦枚舉實現單例,甚至說了它是單例實現的最好寫法.
雖然我還沒有應用過枚舉實現單例,但是很快我就會將它加進我的代碼庫裏.
總結
在單例實現中,我們需要注意以下三個問題:
- (重要)延遲加載,避免浪費.
- (重要)線程安全,避免多個實例.
- 序列化安全.
完。
ChangeLog
2019-01-31 完成
以上皆爲個人所思所得,如有錯誤歡迎評論區指正。
歡迎轉載,煩請署名並保留原文鏈接。