JAVA-單例模式

首先什么是單例在跳?就一條基本原則拧篮,單例對(duì)象的類只會(huì)被初始化一次。在 Java 中螺垢,我們可以說(shuō)在 JVM 中只存在該類的唯一一個(gè)對(duì)象實(shí)例喧务。而要實(shí)現(xiàn)一個(gè)安全的單例對(duì)象,需要考慮一下幾個(gè)問(wèn)題:

  • 你的單例線程安全嗎?

  • 你的單例反射安全嗎枉圃?

  • 你的單例序列化安全嗎功茴?

一、單例的一般實(shí)現(xiàn)
1孽亲、餓漢式

public class HungrySingleton {

    private static final HungrySingleton mInstance = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return mInstance;
    }
}

私有構(gòu)造器是單例的一般套路坎穿,保證不能在外部新建對(duì)象。餓漢式在類加載時(shí)期就已經(jīng)初始化實(shí)例墨林,由于類加載過(guò)程是線程安全的赁酝,所以餓漢式默認(rèn)也是線程安全的。它的缺點(diǎn)也很明顯旭等,我真正需要單例對(duì)象的時(shí)機(jī)是我調(diào)用 getInstance() 的時(shí)候酌呆,而不是類加載時(shí)期。如果單例對(duì)象是很耗資源的搔耕,如數(shù)據(jù)庫(kù)隙袁,socket 等等,無(wú)疑是不合適的弃榨。于是就有了懶漢式菩收。

2、懶漢式

public class LazySingleton {

    private static LazySingleton mInstance;

    private LazySingleton() {
    }

    public static synchronized LazySingleton getInstance() {
        if (mInstance == null)
            mInstance = new LazySingleton();
        return mInstance;
    }
}

實(shí)例化的時(shí)機(jī)挪到了 getInstance() 方法中鲸睛,做到了 lazy init 娜饵,但也失去了類加載時(shí)期初始化的線程安全保障。因此使用了 synchronized 關(guān)鍵字來(lái)保障線程安全官辈。但這顯然是一個(gè)無(wú)差別攻擊箱舞,管你要不要同步遍坟,管你是不是多線程,一律給我加鎖晴股。這也帶來(lái)了額外的性能消耗愿伴。這點(diǎn)問(wèn)題肯定難不倒程序員們,于是电湘,雙重檢查鎖定(DCL, Double Check Lock) 應(yīng)運(yùn)而生隔节。

3、DCL

public class DCLSingleton {
    private static DCLSingleton mInstance;
    private DCLSingleton() {
    }
    public static DCLSingleton getInstance() {
        if (mInstance == null) {                    // 1
            synchronized (DCLSingleton.class) {     // 2
                if (mInstance == null)              // 3
                    mInstance = new DCLSingleton(); // 4
            }
        }
        return mInstance;
    }
}

1 處做第一次判斷寂呛,如果已經(jīng)實(shí)例化了怎诫,直接返回對(duì)象,避免無(wú)用的同步消耗昧谊。2 處僅對(duì)實(shí)例化過(guò)程做同步操作刽虹,保證單例。3 處做第二次判斷呢诬,只有 mInstance 為空時(shí)再初始化∨昼停看起來(lái)時(shí)多么的完美尚镰,保證線程安全的同時(shí)又兼顧性能。但是 DCL 存在一個(gè)致命缺陷哪廓,就是重排序?qū)е碌亩嗑€程訪問(wèn)可能獲得一個(gè)未初始化的對(duì)象狗唉。

首先記住上面標(biāo)記的 4 行代碼。其中第 4 行代碼 mInstance = new DCLSingleton(); 在 JVM 看來(lái)有這么幾步:

1. 為對(duì)象分配內(nèi)存空間
2. 初始化對(duì)象
3. 將 mInstance 引用指向第 1 步中分配的內(nèi)存地址

在單線程內(nèi)涡真,在不影響執(zhí)行結(jié)果的前提下分俯,可能存在指令重排序。例如下列代碼:
int a = 1;
int b = 2;
在 JVM 中你是無(wú)法確保這兩行代碼誰(shuí)先執(zhí)行的哆料,因?yàn)檎l(shuí)先執(zhí)行都不影響程序運(yùn)行結(jié)果缸剪。同理,創(chuàng)建實(shí)例對(duì)象的三部中东亦,第 2 步 初始化對(duì)象 和 第 3 步 將 mInstance 引用指向?qū)ο蟮膬?nèi)存地址 之間也是可能存在重排序的杏节。

為對(duì)象分配內(nèi)存空間
將 mInstance 引用指向第 1 步中分配的內(nèi)存地址
初始化對(duì)象

這樣的話,就存在這樣一種可能典阵。線程 A 按上面重排序之后的指令執(zhí)行奋渔,當(dāng)執(zhí)行到第 2 行 將 mInstance 引用指向?qū)ο蟮膬?nèi)存地址 時(shí),線程 B 開(kāi)始執(zhí)行了壮啊,此時(shí)線程 A 已為 mInstance 賦值嫉鲸,線程 B 進(jìn)行 DCL 的第一次判斷 if (mInstance == null) ,結(jié)果為 false,直接返回 mInstance 指向的對(duì)象歹啼,但是由于重排序的緣故玄渗,對(duì)象其實(shí)尚未初始化减江,這樣就出問(wèn)題了。還挺繞口的捻爷,借用 《Java 并發(fā)編程藝術(shù)》 中的一張表格辈灼,會(huì)對(duì)執(zhí)行流程更加清晰。

時(shí)間 線程 A 線程 B
t1 A1: 分配對(duì)象的內(nèi)存空間
t2 A3: 設(shè)置 mInstance 指向內(nèi)存空間
t3 B1: 判斷 mInstance 是否為空
t4 B2: 由于 mInstance 不為空也榄,線程 B 將訪問(wèn) mInstance 指向的對(duì)象
t5 A2: 初始化對(duì)象
t6 A3: 訪問(wèn) mInstance 引用的對(duì)象 A3 和 A2 發(fā)生重排序?qū)е戮€程 B 獲取了一個(gè)尚未初始化的對(duì)象巡莹。

說(shuō)了半天,該怎么改甜紫?其實(shí)很簡(jiǎn)單降宅,禁止多線程下的重排序就可以了,只需要用 volatile 關(guān)鍵字修飾 mInstance 囚霸。在 JDK 1.5 中腰根,增強(qiáng)了 volatile 的內(nèi)存語(yǔ)義,對(duì)一個(gè)volatile 域的寫拓型,happens-before 于任意后續(xù)對(duì)這個(gè) volatile 域的讀额嘿。volatile 會(huì)禁止一些處理器重排序,此時(shí) DCL 就做到了真正的線程安全劣挫。

4册养、靜態(tài)內(nèi)部類模式

public class StaticInnerSingleton {

    private StaticInnerSingleton(){}

    private static class SingletonHolder{
        private static final StaticInnerSingleton mInstance=new StaticInnerSingleton();
    }

    public static StaticInnerSingleton getInstance(){
        return SingletonHolder.mInstance;
    }
}

鑒于 DCL 繁瑣的代碼,程序員又發(fā)明了靜態(tài)內(nèi)部類模式压固,它和餓漢式一樣基于類加載時(shí)器的線程安全球拦,但是又做到了延遲加載。SingletonHolder 是一個(gè)靜態(tài)內(nèi)部類帐我,當(dāng)外部類被加載的時(shí)候并不會(huì)初始化坎炼。當(dāng)調(diào)用 getInstance() 方法時(shí),才會(huì)被加載拦键。

枚舉單例暫且不提谣光,放在最后再說(shuō)。先對(duì)上面的單例模式做個(gè)檢測(cè)矿咕。

二抢肛、真的是單例?
還記得開(kāi)頭的提問(wèn)嗎碳柱?
你的單例線程安全嗎?
你的單例反射安全嗎捡絮?
你的單例序列化安全嗎?

上面大篇幅的論述都在說(shuō)明線程安全莲镣。下面看看反射安全和序列化安全福稳。

1、反射安全
直接上代碼瑞侮,我用 DCL 來(lái)做測(cè)試:

public static void main(String[] args) {
    DCLSingleton singleton1 = DCLSingleton.getInstance();
    DCLSingleton singleton2 = null;

    try {
        Class<DCLSingleton> clazz = DCLSingleton.class;
        Constructor<DCLSingleton> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        singleton2 = constructor.newInstance();
    } catch (Exception e) {
        e.printStackTrace();
    }
    System.out.println(singleton1.hashCode());
    System.out.println(singleton2.hashCode());
}

執(zhí)行結(jié)果:
1627674070
1360875712
很無(wú)情的圆,通過(guò)反射破壞了單例鼓拧。如何保證反射安全呢?只能以暴制暴越妈,當(dāng)已經(jīng)存在實(shí)例的時(shí)候再去調(diào)用構(gòu)造函數(shù)直接拋出異常季俩,對(duì)構(gòu)造函數(shù)做如下修改:

private DCLSingleton() {
    if (mInstance!=null)
        throw new RuntimeException("想反射我,沒(méi)門梅掠!");
}

上面的測(cè)試代碼會(huì)直接拋出異常酌住。

2、序列化安全
將你的單例類實(shí)現(xiàn) Serializable 持久化保存起來(lái)阎抒,日后再恢復(fù)出來(lái)酪我,他還是單例嗎?

public static void main(String[] args) {

    DCLSingleton singleton1 = DCLSingleton.getInstance();
    DCLSingleton singleton2 = null;

    try {
        ObjectOutput output=new ObjectOutputStream(new FileOutputStream("singleton.ser"));
        output.writeObject(singleton1);
        output.close();

        ObjectInput input=new ObjectInputStream(new FileInputStream("singleton.ser"));
        singleton2= (DCLSingleton) input.readObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
    System.out.println(singleton1.hashCode());
    System.out.println(singleton2.hashCode());

}

執(zhí)行結(jié)果:

644117698
793589513
不堪一擊且叁。反序列化時(shí)生成了新的實(shí)例對(duì)象都哭。要修復(fù)也很簡(jiǎn)單,只需要修改反序列化的邏輯就可以了逞带,即重寫 readResolve() 方法欺矫,使其返回統(tǒng)一實(shí)例。

protected Object readResolve() {
    return getInstance();
}

脆弱不堪的單例模式經(jīng)過(guò)重重考驗(yàn)掰担,進(jìn)化成了完全體汇陆,延遲加載,線程安全带饱,反射安全,序列化安全阅羹。全部代碼如下:

public class DCLSingleton implements Serializable {

    private static DCLSingleton mInstance;

    private DCLSingleton() {
        if (mInstance!=null)
            throw new RuntimeException("想反射我勺疼,沒(méi)門!");
    }

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

    protected Object readResolve() {
        return getInstance();
    }
}

三捏鱼、枚舉單例
在介紹利用枚舉實(shí)現(xiàn)單例模式的原理执庐,先介紹一些相關(guān)的基礎(chǔ)內(nèi)容。
首先导梆,枚舉類似類轨淌,一個(gè)枚舉可以擁有成員變量,成員方法看尼,構(gòu)造方法递鹉。先來(lái)看枚舉最基本的用法:

enum Type{
    A,B,C,D;
}

創(chuàng)建enum時(shí),編譯器會(huì)自動(dòng)為我們生成一個(gè)繼承自java.lang.Enum的類藏斩,我們上面的enum可以簡(jiǎn)單看作:

class Type extends Enum{
    public static final Type A;
    public static final Type B;
    ...
}

對(duì)于上面的例子躏结,我們可以把Type看作一個(gè)類,而把A狰域,B媳拴,C黄橘,D看作類的Type的實(shí)例。
當(dāng)然屈溉,這個(gè)構(gòu)建實(shí)例的過(guò)程不是我們做的塞关,一個(gè)enum的構(gòu)造方法限制是private的,也就是不允許我們調(diào)用子巾。

“類”方法和“實(shí)例”方法

上面說(shuō)到帆赢,我們可以把Type看作一個(gè)類,而把A砰左,B匿醒。。缠导×幔看作Type的一個(gè)實(shí)例。同樣僻造,在enum中憋他,我們可以定義類和實(shí)例的變量以及方法∷柘鳎看下面的代碼:

enum Type{
    A,B,C,D;
    static int value;
    public static int getValue() {
        return value;
    }
    String type;
    public String getType() {
        return type;
    }
}

在原有的基礎(chǔ)上竹挡,添加了類方法和實(shí)例方法。我們把Type看做一個(gè)類立膛,那么enum中靜態(tài)的域和方法揪罕,都可以視作類方法。和我們調(diào)用普通的靜態(tài)方法一樣宝泵,這里調(diào)用類方法也是通過(guò) Type.getValue()即可調(diào)用好啰,訪問(wèn)類屬性也是通過(guò)Type.value即可訪問(wèn)。
下面的是實(shí)例方法儿奶,也就是每個(gè)實(shí)例才能調(diào)用的方法框往。那么實(shí)例是什么呢?沒(méi)錯(cuò)闯捎,就是A椰弊,B,C瓤鼻,D秉版。所以我們調(diào)用實(shí)例方法,也就通過(guò) Type.A.getType()來(lái)調(diào)用就可以了娱仔。
最后沐飘,對(duì)于某個(gè)實(shí)例而言,還可以實(shí)現(xiàn)自己的實(shí)例方法。再看下下面的代碼:

enum Type{
A{
    public String getType() {
        return "I will not tell you";
    }
},B,C,D;
static int value;

public static int getValue() {
    return value;
}

String type;
public String getType() {
    return type;
 }
}

這里耐朴,A實(shí)例后面的{…}就是屬于A的實(shí)例方法借卧,可以通過(guò)覆蓋原本的方法,實(shí)現(xiàn)屬于自己的定制筛峭。
除此之外铐刘,我們還可以添加抽象方法在enum中,強(qiáng)制ABCD都實(shí)現(xiàn)各自的處理邏輯:

enum Type{
    A{
        public String getType() {
            return "A";
        }
    },B {
        @Override
        public String getType() {
            return "B";
        }
    },C {
        @Override
        public String getType() {
            return "C";
        }
    },D {
        @Override
        public String getType() {
            return "D";
        }
    };

    public abstract String getType();
}

枚舉單例

有了上面的基礎(chǔ)影晓,我們可以來(lái)看一下枚舉單例的實(shí)現(xiàn)方法:

public enum SomeThing {
    INSTANCE;
   public String get(){     return "SomeThing";} 
} 
main(){
    SomeThing something = SomeThing.INSTANCE;
    someThing.get(); 
}

上面的類Resource是我們要應(yīng)用單例模式的資源镰吵,具體可以表現(xiàn)為網(wǎng)絡(luò)連接,數(shù)據(jù)庫(kù)連接挂签,線程池等等疤祭。
獲取資源的方式很簡(jiǎn)單,只要 SomeThing.INSTANCE.getInstance() 即可獲得所要實(shí)例饵婆。下面我們來(lái)看看單例是如何被保證的:
首先勺馆,在枚舉中我們明確了構(gòu)造方法限制為私有,在我們?cè)L問(wèn)枚舉實(shí)例時(shí)會(huì)執(zhí)行構(gòu)造方法侨核,同時(shí)每個(gè)枚舉實(shí)例都是static final類型的草穆,也就表明只能被實(shí)例化一次。在調(diào)用構(gòu)造方法時(shí)搓译,我們的單例被實(shí)例化悲柱。
也就是說(shuō),因?yàn)閑num中的實(shí)例被保證只會(huì)被實(shí)例化一次些己,所以我們的INSTANCE也被保證實(shí)例化一次豌鸡。
可以看到,枚舉實(shí)現(xiàn)單例還是比較簡(jiǎn)單的段标,除此之外我們?cè)賮?lái)看一下Enum這個(gè)類的聲明:

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable

可以看到直颅,枚舉也提供了序列化機(jī)制。某些情況怀樟,比如我們要通過(guò)網(wǎng)絡(luò)傳輸一個(gè)數(shù)據(jù)庫(kù)連接的句柄,會(huì)提供很多幫助盆佣。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末往堡,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子共耍,更是在濱河造成了極大的恐慌虑灰,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痹兜,死亡現(xiàn)場(chǎng)離奇詭異穆咐,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門对湃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)崖叫,“玉大人,你說(shuō)我怎么就攤上這事拍柒⌒目” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵拆讯,是天一觀的道長(zhǎng)脂男。 經(jīng)常有香客問(wèn)我,道長(zhǎng)种呐,這世上最難降的妖魔是什么宰翅? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮爽室,結(jié)果婚禮上汁讼,老公的妹妹穿的比我還像新娘。我一直安慰自己肮之,他們只是感情好掉缺,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著戈擒,像睡著了一般眶明。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上筐高,一...
    開(kāi)封第一講書(shū)人閱讀 51,578評(píng)論 1 305
  • 那天搜囱,我揣著相機(jī)與錄音,去河邊找鬼柑土。 笑死蜀肘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的稽屏。 我是一名探鬼主播扮宠,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼狐榔!你這毒婦竟也來(lái)了坛增?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤薄腻,失蹤者是張志新(化名)和其女友劉穎收捣,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體庵楷,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡罢艾,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年楣颠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片咐蚯。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡童漩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出仓蛆,到底是詐尸還是另有隱情睁冬,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布看疙,位于F島的核電站豆拨,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏能庆。R本人自食惡果不足惜施禾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望搁胆。 院中可真熱鬧弥搞,春花似錦、人聲如沸渠旁。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)顾腊。三九已至粤铭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間杂靶,已是汗流浹背梆惯。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留吗垮,地道東北人垛吗。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像烁登,于是被迫代替她去往敵國(guó)和親怯屉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 主要參考自 菜鳥(niǎo)教程 單例模式是JAVA中最簡(jiǎn)單的模式之一饵沧,這種模式屬于創(chuàng)建型模式蚀之,它提供了一種創(chuàng)建對(duì)象的最...
    東溪95閱讀 562評(píng)論 0 2
  • 簡(jiǎn)介 單例模式是應(yīng)用最廣的模式之一,它是為了確保某一個(gè)類在一個(gè)java虛擬機(jī)(進(jìn)程)中有且只有一個(gè)實(shí)例存在. 帶來(lái)...
    JimmieYang閱讀 2,103評(píng)論 0 15
  • 一、前言 作為對(duì)象的創(chuàng)建模式捷泞,單例模式確保某一個(gè)類只有一個(gè)實(shí)例,而且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例寿谴。這個(gè)類稱為...
    manimaniho閱讀 442評(píng)論 0 0
  • 1. 什么是單例模式 Java中單例(Singleton)模式是一種廣泛使用的設(shè)計(jì)模式锁右。單例模式的主要作用是保證在...
    洋芋掉到碗里去了閱讀 644評(píng)論 0 4
  • 目錄一.什么是單例?二.有幾種?三.應(yīng)用場(chǎng)景四.注意的地方 一.什么是單例咏瑟? 單例模式 保證一個(gè)類在內(nèi)存中只有一個(gè)...
    在挖坑的猿閱讀 854評(píng)論 0 0