設(shè)計(jì)模式(一)--深入單例模式(涉及線程安全問題)

這又是一個(gè)新的系列啦,探究各大設(shè)計(jì)模式在開發(fā)中必須注意思考的一些問題,以及它們的多向使用。

文章結(jié)構(gòu):(1)單例模式概念以及優(yōu)缺點(diǎn)(2)各式各樣的單例及其線程安全問題。(3)使用推薦。


單例模式概念以及優(yōu)缺點(diǎn):

(1)定義:

要求一個(gè)類只能生成一個(gè)對(duì)象,所有對(duì)象對(duì)它的依賴相同。

(2)優(yōu)點(diǎn):

1. 只有一個(gè)實(shí)例,減少內(nèi)存開支。應(yīng)用在一個(gè)經(jīng)常被訪問的對(duì)象上

2. 減少系統(tǒng)的性能開銷,應(yīng)用啟動(dòng)時(shí),直接產(chǎn)生一單例對(duì)象,用永久駐留內(nèi)存的方式。

3.避免對(duì)資源的多重占用

4.可在系統(tǒng)設(shè)置全局的訪問點(diǎn),優(yōu)化和共享資源訪問。

(3)缺點(diǎn):

1.一般沒有接口,擴(kuò)展困難。原因:接口對(duì)單例模式?jīng)]有任何意義;要求“自行實(shí)例化”,并提供單一實(shí)例,接口或抽象類不可能被實(shí)例化。(當(dāng)然,單例模式可以實(shí)現(xiàn)接口、被繼承,但需要根據(jù)系統(tǒng)開發(fā)環(huán)境判斷)

2.單例模式對(duì)測(cè)試是不利的。如果單例模式?jīng)]完成,是不能進(jìn)行測(cè)試的。

3.單例模式與單一職責(zé)原則有沖突。原因:一個(gè)類應(yīng)該只實(shí)現(xiàn)一個(gè)邏輯,而不關(guān)心它是否是單例,是不是要單例取決于環(huán)境;單例模式把“要單例”和業(yè)務(wù)邏輯融合在一個(gè)類。

(4)使用場(chǎng)景:

1.要求生成唯一序列化的環(huán)境

2.項(xiàng)目需要的一個(gè)共享訪問點(diǎn)或共享的數(shù)據(jù)點(diǎn)

3.創(chuàng)建一個(gè)對(duì)象需要消耗資源過多的情況。如:要訪問IO和 數(shù)據(jù)庫等資源。

4.需要定義大量的靜態(tài)常量和靜態(tài)方法(如工具類)的環(huán)境。可以采用單例模式或者直接聲明static的方式。

(5)注意事項(xiàng):

1.類中其他方法,盡量是static

2.注意JVM的垃圾回收機(jī)制。

如果一個(gè)單例對(duì)象在內(nèi)存長(zhǎng)久不使用,JVM就認(rèn)為對(duì)象是一個(gè)垃圾。所以如果針對(duì)一些狀態(tài)值,如果回收的話,應(yīng)用就會(huì)出現(xiàn)故障。

3.采用單例模式來記錄狀態(tài)值的類的兩大方法:

(一)、由容器管理單例的生命周期。Java EE容器或者框架級(jí)容器,自行管理對(duì)象的生命周期。
(二)狀態(tài)隨時(shí)記錄。異步記錄的方式或者使用觀察者模式,記錄狀態(tài)變化,確保重新初始化也可從資源環(huán)境獲得銷毀前的數(shù)據(jù)。

二、各式各樣的單例及其線程安全問題:

(1)懶漢式單例:

意思:就是需要使用這個(gè)對(duì)象的時(shí)候才去創(chuàng)建這個(gè)對(duì)象。

//懶漢式單例
public class Singleton1 {
    private static Singleton1 singleton1=null;
    public Singleton1(){

    }
    public static Singleton1 getInstance(){
        if (singleton1==null){
            try {
                Thread.sleep(200);//我們知道初始化一個(gè)對(duì)象需要一定時(shí)間的嘛,我們用sleep假設(shè)這個(gè)時(shí)間
                singleton1 = new Singleton1();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return singleton1;
    }
}
//測(cè)試線程
public class SingleThread1 extends Thread {
//哈希值對(duì)應(yīng)的是唯一的嘛,如果不一樣了,就說明使用的不是同一個(gè)對(duì)象咯。
    @Override
    public void run() {
        System.out.println(Singleton1.getInstance().hashCode());
    }

}

//測(cè)試類
public class SingletonTest {

    public static void main(String []args){
        SingleThread1[] thread1s = new SingleThread1[10];
        for (int i= 0;i<thread1s.length;i++){
            thread1s[i] = new SingleThread1();
        }
        for (int j = 0; j < thread1s.length; j++) {
            thread1s[j].start();
        }
    }
}
//打印的結(jié)果:
569219718
1259146238
565373737
732830316
679555294
1886445805
1557403724
635681435
622018771
1439317371

線程安全的懶漢式單例設(shè)計(jì):

1.鎖住獲取方法方式:

public class Singleton3 {
    private static Singleton3 instance = null;

    private Singleton3(){}
    //鎖住獲取方法的方式
    public synchronized static Singleton3 getInstance() {
        try {
            if(instance != null){
            }else{
                Thread.sleep(300);
                instance = new Singleton3();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return instance;
    }
}

2.鎖住部分代碼塊的方式:

public class Singleton2 {
    private static Singleton2 instance = null;
     private Singleton2(){
     }
    public static Singleton2 getInstance() {
        try {
            //鎖住代碼塊的方式
            synchronized (Singleton2.class) {
                if(instance != null){

                }else{
                    Thread.sleep(200);
                    instance = new Singleton2();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return instance;
    }
}

3.鎖住初始化對(duì)象操作的方式:但是!!!這不是線程安全的!!一會(huì)有這個(gè)方式的優(yōu)化從而實(shí)現(xiàn)線程安全。

為什么??

因?yàn)槎鄠€(gè)訪問已經(jīng)進(jìn)入到創(chuàng)建的那里了。

public class Singleton4 {
    private static Singleton4 instance = null;
    private Singleton4(){}
    public static Singleton4 getInstance() {
        try {
            if(instance != null){
            }else{
                Thread.sleep(300);
                //只鎖住初始化操作的方式
                synchronized (Singleton4.class) {
                    instance = new Singleton4();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return instance;
    }
}

4.鎖住初始化對(duì)象操作的方式,但有個(gè)再檢查操作:

public class Singleton5 {
    //使用volatile關(guān)鍵字保其可見性
    volatile private static Singleton5 instance = null;
    private Singleton5(){}
    public static Singleton5 getInstance() {
        try {
            if(instance != null){
            }else{
                Thread.sleep(300);
                //鎖住初始化操作的方式
                synchronized (Singleton5.class) {
                    if(instance == null){//二次檢查
                        instance = new Singleton5();
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return instance;
    }
}

使用了volatile關(guān)鍵字來保證其線程間的可見性;在同步代碼塊中使用二次檢查,以保證其不被重復(fù)實(shí)例化。集合其二者,這種實(shí)現(xiàn)方式既保證了其高效性,也保證了其線程安全性。

解析volatile在此的作用:

volatile(涉及java內(nèi)存模型的知識(shí))會(huì)禁止CPU對(duì)內(nèi)存訪問重排序(并不一定禁止指令重排),也就是CPU執(zhí)行初始化操作,那么他會(huì)保證其他CPU看到的操作順序是1.給 instance 分配內(nèi)存--2.調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量--3.將instance對(duì)象指向分配的內(nèi)存空間(執(zhí)行完這步 instance 就為非 null 了),(雖然在CPU內(nèi)由于流水線多發(fā)射并不一定是這個(gè)順序)

不使用volatile的問題是什么呢??

在 JVM 的即時(shí)編譯器中存在指令重排序的優(yōu)化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執(zhí)行完畢、2 未執(zhí)行之前,被線程二搶占了,這時(shí) instance 已經(jīng)是非 null 了(但卻沒有初始化),所以線程二會(huì)直接返回 instance,然后使用,然后順理成章地報(bào)錯(cuò)。

用volatile的意義并不在于其他線程一定要去內(nèi)存總讀取instance,而在于它限制了CPU對(duì)內(nèi)存操作的重拍序,使其他線程在看到3之前2一定是執(zhí)行過的。


(2)餓漢式單例:

意思是:類裝載時(shí)就實(shí)例化該單例類

public class Singleton6 {
    //一初始化類就初始化這個(gè)單例了!!!
    private static Singleton6 singleton6= new Singleton6();
    private Singleton6(){

    }
    public static Singleton6 getInstance(){
        return singleton6;
    }
}

基于classloder機(jī)制避免了多線程的同步問題,不過,instance在類裝載時(shí)就實(shí)例化,雖然導(dǎo)致類裝載的原因有很多種,在單例模式中大多數(shù)都是調(diào)用getInstance方法, 但是也不能確定有其他的方式(或者其他的靜態(tài)方法)導(dǎo)致類裝載,這時(shí)候初始化instance顯然沒有達(dá)到lazy loading的效果。這個(gè)是沒有懶加載的功能的!!!

餓漢式單例變種:

public class Singleton7 {
    private static Singleton7 instance = null;
    static {
        instance = new Singleton7();
    }
    private Singleton7() {
    }
    public static Singleton7 getInstance() {
        return instance;
    }
}

(3)靜態(tài)內(nèi)部類實(shí)現(xiàn)懶加載:

//靜態(tài)內(nèi)部類單例
public class Singleton8 {
    private static class SingletonHolder {
        private static final Singleton8 INSTANCE = new Singleton8();
    }
    private Singleton8 (){}
    public static final Singleton8 getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

同樣利用了classloder的機(jī)制來保證初始化instance時(shí)只有一個(gè)線程,它跟餓漢式的兩種方式不同的是:餓漢式的兩種方式是只要Singleton類被裝載了,那么instance就會(huì)被實(shí)例化(沒有達(dá)到lazy loading效果),而這種方式是Singleton類被裝載了,instance還未被初始化。因?yàn)镾ingletonHolder類沒有被主動(dòng)使用,只有顯示通過調(diào)用getInstance方法時(shí),才會(huì)顯示裝載SingletonHolder類,從而實(shí)例化instance。想象一下,如果實(shí)例化instance很消耗資源,我想讓他延遲加載,另外一方面,我不希望在Singleton類加載時(shí)就實(shí)例化,因?yàn)槲也荒艽_保Singleton類還可能在其他的地方被主動(dòng)使用從而被加載,那么這個(gè)時(shí)候?qū)嵗痠nstance顯然是不合適的。

靜態(tài)內(nèi)部類方式單例再度研究:序列化和反序列化問題:

public class MySingleton implements Serializable {

    private static final long serialVersionUID = 1L;

    //內(nèi)部類
    private static class MySingletonHandler{
        private static MySingleton instance = new MySingleton();
    }

    private MySingleton(){}

    public static MySingleton getInstance() {
        return MySingletonHandler.instance;
    }
}
public class SaveAndReadForSingleton {
    public static void main(String[] args) {
        MySingleton singleton = MySingleton.getInstance();
        //創(chuàng)建個(gè)文件流
        File file = new File("MySingleton.txt");
        //使用節(jié)點(diǎn)流,直接與文件關(guān)聯(lián)
        try {
            //寫入文件
            FileOutputStream fos = new FileOutputStream(file);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(singleton);
            fos.close();
            oos.close();
            System.out.println(singleton.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            //讀取文件流
            FileInputStream fis = new FileInputStream(file);
            ObjectInputStream ois = new ObjectInputStream(fis);
            MySingleton rSingleton = (MySingleton) ois.readObject();
            fis.close();
            ois.close();
            System.out.println(rSingleton.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}

這樣的單例測(cè)試出來時(shí),hash是不一樣的,因?yàn)闆]有同步到序列化與反序列化問題。說明反序列化后返回的對(duì)象是重新實(shí)例化的,單例被破壞了。

解決:當(dāng)JVM從內(nèi)存中反序列化地"組裝"一個(gè)新對(duì)象時(shí),就會(huì)自動(dòng)調(diào)用readResolve方法來返回我們指定好的對(duì)象,readResolve允許class在反序列化返回對(duì)象前替換、解析在流中讀出來的對(duì)象。實(shí)現(xiàn)readResolve方法,一個(gè)class可以直接控制反序化返回的類型和對(duì)象引用。

public class MySingleton1 implements Serializable {

    private static final long serialVersionUID = 1L;

    //內(nèi)部類
    private static class MySingletonHandler{
        private static MySingleton1 instance = new MySingleton1();
    }

    private MySingleton1(){}

    public static MySingleton1 getInstance() {
        return MySingletonHandler.instance;
    }

    //該方法在反序列化時(shí)會(huì)被調(diào)用,該方法不是接口定義的方法,有點(diǎn)兒約定俗成的感覺
    protected Object readResolve() throws ObjectStreamException {
        System.out.println("調(diào)用了readResolve方法!");
        return MySingletonHandler.instance;
    }
}

修改SaveAndReadForSingleton文件中的MySingleton,輸出

2133927002
調(diào)用了readResolve方法!解決序列化與反序列化問題!
2133927002

(4)枚舉:

//枚舉實(shí)現(xiàn)單例
public enum EnumSingletonFactory {
    singletonFactory;
    private EnumSingleton instance;
    private EnumSingletonFactory(){//枚舉類的構(gòu)造方法在類加載是被實(shí)例化
        instance = new EnumSingleton();
    }
    public EnumSingleton getInstance(){
        return instance;
    }
}

在thread中調(diào)用實(shí)現(xiàn):

@Override  
    public void run() {     System.out.println(EnumFactory.singletonFactory.getInstance().hashCode());  
    }

但是此博客 引起我思考,是違反單一職責(zé)的,因?yàn)樗┞读嗣杜e的細(xì)節(jié),所以我們需要改造他。

//使用工廠來生成枚舉類
//通過工廠類的靜態(tài)方法去訪問枚舉類,然后通過枚舉類訪問它的單例。
public class ClassFactory {
    private enum MyEnumSingleton{
        singletonFactory;

        private EnumSingleton instance;

        private MyEnumSingleton(){//枚舉類的構(gòu)造方法在類加載是被實(shí)例化
            instance = new EnumSingleton();
        }

        public EnumSingleton getInstance(){
            return instance;
        }
    }

    public static EnumSingleton getInstance(){
        return MyEnumSingleton.singletonFactory.getInstance();
    }
}

在thread中調(diào)用實(shí)現(xiàn):

 @Override  
    public void run() {   
        System.out.println(ClassFactory.getInstance().hashCode());  
    }  

枚舉類的方式不僅能避免多線程同步問題,而且還能防止反序列化重新創(chuàng)建新的對(duì)象。不過實(shí)際工程代碼中,很少去用此方式。


三、推薦使用:

上述的各種單例都講完了:基本是五種寫法。懶漢,惡漢,雙重校驗(yàn)鎖,枚舉和靜態(tài)內(nèi)部類。

(1)餓漢式單例。

原因:類的加載機(jī)制保證了,類初始化時(shí),只執(zhí)行一次靜態(tài)代碼塊以及類變量初始化。直接保證了唯一性,保證了線程安全。(一般使用非靜態(tài)代碼塊方式)

(2)靜態(tài)內(nèi)部類方式:

原因:懶加載唄!!!應(yīng)用在一些十分巨大的單例bean中。


參考博客:此博客讓我對(duì)單例加深了一大層,感謝感謝!!

http://blog.csdn.net/cselmu9/article/details/51366946


好了,設(shè)計(jì)模式(一)--深入單例模式(涉及線程安全問題)講完了。本博客是我復(fù)習(xí)階段的一些筆記,拿來分享經(jīng)驗(yàn)給大家。歡迎在下面指出錯(cuò)誤,共同學(xué)習(xí)!!你的點(diǎn)贊是對(duì)我最好的支持!!

更多內(nèi)容,可以訪問JackFrost的博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,595評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,814評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,224評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,444評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,988評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,804評(píng)論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,998評(píng)論 1 370
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評(píng)論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,237評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評(píng)論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,706評(píng)論 3 393
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,993評(píng)論 2 374

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