孤獨的單身羅漢——單例模式

單例模式

來了來了,他來了。大家好,我是老王。今天給大家帶來的是23種設計模式的第一種——單例模式。前天寫的第一篇文章設計模式的七大原則,是我第一次寫文章,目前看了瀏覽量,讓我信心大增。文章有什麼講述方式不對的歡迎大家隨時給我私信。24小時恭候!!(創作不易,還望各位仁兄看完舉起小手點個小贊。)

一、引言

單例模式是啥????故名思意,就是單着的意思 = =!沒錯,就是爲了來保證整個系統運行中,從頭至尾只有一個對象。比如說,我們最可愛的學校,可以有很多學生,可以有很多主任,但是不能有很多校長。爲什麼?因爲要確保只有一個校長,學校這個系統纔不會因爲受干擾崩潰,所以單例模式應運而生。

二、實現方式

都知道了單例模式是幹嘛的了,那就好辦了。首先你要確保整個系統的laowang類只有老王一個對象 最重要的前提你要做什麼??可想而知,老王不能被其他類所創造出來啊。
因此有如下做法:
1. 先把構造方法給私有化了(private)。
2. 接着在程序運行的時候創建一個對象放在內存裏就得了。

你沒看錯,要實現單例模式,確確實實就只有這兩步,第一步2秒鐘搞定,第二步就是我們要來探討的部分了。
實現單例模式有五種做法:

  • 餓漢式:
    也就是在程序裝載時提前把對象創建了,有人來就給他。
  • 懶漢式:
    在有人需要的時候,再創建第一個對象,然後再給他。(懶加載)
  • 雙重檢驗方式:
  • 內部類方式:
  • 枚舉方式:

提示:在上面實現方式中只展現線程安全的做法,詳細的我後面會指出。

三、具體實現

1. 餓漢式

分爲兩步走:

  1. 把構造方法私有化
  2. 在程序裝載時提前創建好實例
class Laowang{
 	private Laowang(){}//私有化構造方法
    private static Laowang laowang=new Laowang();//直接創建靜態實例
    
    //對外提供靜態方法獲取當前的Laowang
    public static Laowang getLaowang(){
        return laowang;
    }
}

//Main方法
public static void main(String[] args) {
		 //用Laowang類的靜態方法getLaowang()獲取實例;
	     Laowang laowang1=Laowang.getLaowang();
	     Laowang laowang2=Laowang.getLaowang();
	     //判斷laowang1是否和老王2是同一個對象(是輸出true,否則false)
	     System.out.println(laowang1==laowang2);
    }
    
運行結果: true

上面這個例子中,在老王這個類中,先私有化構造方法,接着創建一個靜態屬性laowang, 然後提供一個對外的靜態方法getLaowang()可以給別人拿這個laowang(因爲你已經把構造方法私有化了,所以你只能通過靜態方法把laowang給別人。)
Main方法中定義了2個引用laowang1,laowang2,但是都是通過同一種方式拿到實例對象laowang.因此拿到的是同一個對象,所以返回true,這就是餓漢式實現法。

優點:實現簡單,線程安全。
缺點:很明顯,在類裝載的時候直接創建,有時候你不需要它,它也會創 建,造成內存資源浪費。

餓漢式也有另外一種寫法,也是一樣的效果。把new Laowang()放在靜態代碼塊裏,如下:

class Laowang{
    private static Laowang laowang;
    private Laowang(){}//私有化構造方法
    static {
        laowang=new Laowang();
    }

    //對外提供靜態方法獲取當前的Laowang
    public static Laowang getLaowang(){
        return laowang;
    }
}

2. 懶漢式

在程序需要用到調用的時候纔給它(懶加載),因此做法如下:

class Laowang{
    private static Laowang laowang;
    private Laowang(){}//私有化構造方法

    //對外提供靜態方法,創建實例然後返回,當前的Laowang
    public static synchronized Laowang getLaowang(){
        if (laowang == null) {
            laowang=new Laowang();
        }
        return laowang;
    }
}

此做法需要在方法聲明加上synchronized,(具體作用:比如說很多人來訪問這個方法,他們必須排隊訪問) 這種怎麼理解呢?就是說在別人需要用到laowang,調用getLaowang()的時候,先排隊,排到他的時候,進去判斷laowang是不是爲空,是就new一個,不是就拿當前laowang給他。當然main運行結果還是爲true這裏就不作多的描述。

優點: 不會造成內存浪費
缺點: 很明顯,人人平等,大家都要排隊,既然排隊就慢,高併發情況下,極度影響效率

在這裏解釋爲什麼要加同步鎖:如果不加的話,舉個例子,程序運行剛開始,小黑和小紅同時訪問這個方法,同時作判斷,肯定同時都判斷爲空,而且兩個人都進去了,new Laowang();很明顯直接造成laowang不是單例的了。因此要加鎖。 小黑小紅都要排隊。

3. 雙重檢驗鎖

也是屬於懶加載

class Laowang{
    private volatile static Laowang laowang;//必須加上volatile 關鍵字
    private Laowang(){}//私有化構造方法
    //對外提供靜態方法,創建實例然後返回,當前的Laowang
    public static  Laowang getLaowang(){
        if (laowang == null) {
            synchronized (Laowang.class){ //同步代碼(照樣要排隊)
                if (laowang==null){
                    laowang=new Laowang();
                }
            }
        }
        return laowang;
    }
}

分析一下代碼哈,首先靜態屬性laowang要加上volatile (具體作用要詳細瞭解的話建議百度搜一下哈,屬於多線程內容的一部分)。然後再getLaowang()方法中,先判斷laowang是否爲空,如果爲空,請排隊。排完隊後,再次判斷,如果還是爲空,才new一個返回。

舉個例子解釋一下爲什麼要這樣做:
還是小黑小紅同時併發進來訪問,然後肯定同時第一次判斷都爲空,接着兩個人排隊,小黑先進去玩會,肯定第二次判斷爲空,結果肯定是小黑new了一個laowang走了。排到小紅了,小紅進來第二次判斷髮現laowang不爲空了,直接帶走。
溫馨提示:再看一邊再繼續看下面內容


這個時候有人問了,那爲什麼要第一次判斷幹嘛,直接排隊他不香嗎?沒錯,我第一次也這不理解的地方。我們腦回路回退到小黑剛new完laowang走了。剛要排到小紅了。突然來了個第三者小三,如果你沒有第一次判斷,小三還要繼續排在小紅後面,造成效率降低。但是現在小三第一次判斷髮現laowang已經不爲空了(此時laowang是第一個人小黑弄出來的),直接帶走。

優點: 解決了排隊效率降低的問題,線程安全。
缺點: 實現較爲複雜。

4. 內部類方式

也是屬於懶加載,故名思意,首先整個內部類出來,代碼如下:

兄臺要看如下代碼,請先了解final關鍵字的作用
這裏對final作簡單描述:

  1. 對類使用:表示該類不能被繼承(俗稱斷子絕孫類)
  2. 對方法使用:表示該方法不能被重寫
  3. 對基礎類型使用:比如說int,float…表示該值不可以被更改
  4. 對引用對象使用:表示該引用從頭到尾只指向一個對象
    以上3.對基礎類型使用,4.對引用對象使用都必須直接賦值。
class Laowang{
    private  Laowang(){} //私有化構造方法

    //對外提供靜態方法,調用內部類的屬性,返回
    public static final Laowang getLaowang(){
        return laowangHolder.INSTANCE;
    }
    //靜態內部類
    private static final class LaowangHolder{
        private static final Laowang INSTANCE =new Laowang();
    }
}

解釋以上代碼:首先聲明瞭一個內部類(LaowangHolder),他有個靜態且被final修飾的屬性INSTANCE,因此需要直接賦值,new Laowang();接着在getLaowang()方法中調用內部類的INSTANCE屬性,返回。因爲INSTANCE被final修飾,只指向同一個laowang,所以他是單例的。

5. 枚舉方式

這些方法實現相對簡單,所以直接上代碼:

enum Laowang{
    laowang;
    public void whateverMethod(){}
}

//Main方法:
public static void main(String[] args) {
		//直接當成屬性調用就可以了
        Laowang laowang1=Laowang.laowang;
        Laowang laowang2=Laowang.laowang;
        System.out.println(laowang1==laowang2);
    }

直接聲明一個枚舉類,定義一個屬性,main方法中直接獲取即可。

四、總結

單例模式線程安全的就這幾種了,反正我自己是搞懂了,希望你們也能看懂。想看對單例瞭解更深入可以點我下方的鏈接。看有經驗的人說,單例是最常用的也是最簡單的一種設計模式之一,在工作中實現方式大都選擇餓漢式,或者內部類,枚舉, 但是懶漢,雙重校驗鎖就比較少了。
學完之後自己想在之前項目中實現一個單例的Map,用來保存登陸驗證時的token,就不用每次都往數據庫裏存了,算是做了一層緩存,提高性能吧。
好了,本文到此爲止,希望大家都能有所收穫。
此文知識點借鑑於菜鳥編程——單例模式
創作不易,兄弟姐妹們點贊收藏加關注,你們的鼓勵就是我最大的動力!
未完待續,連載中…

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