吐血整理全網最全的單例模式

前言

以前文章已經說過了設計模式的七大原則,即接口屏蔽原則,開閉原則,依賴倒轉原則,迪米特原則,里氏替換原則,單一職責原則,合成複用原則,不明白的,能夠移至XXXX(代寫)。從今天開始咱們就要學習一些常見的設計模式,方便咱們之後看源碼使用,固然,也能夠指導咱們日常的編碼任務。
設計模式

咱們常見的設計模式主要有23種,分爲3種類型,咱也不全說,只寫重要的幾個把。緩存

建立型:單例模式,工廠模式,原型模式安全

結構型:適配器模式,裝飾模式,代理模式bash

行爲型:模板模式,觀察者模式,狀態模式,責任鏈模式markdown

單例模式的概念和做用

概念

系統中只須要一個全局的實例,好比一些工具類,Converter,SqlSession等。多線程

爲何要用單例模式?

  • 只有一個全局的實例,減小了內存開支,特別是某個對象須要頻繁的建立和銷燬的時候,而建立和銷燬的過程由jvm執行,咱們沒法對其進行優化,因此單例模式的優點就顯現出來啦。
  • 單例模式能夠避免對資源的多重佔用,避免出現多線程的複雜問題。

單例模式的寫法重點

構造方法私有化

咱們須要將構造方法私有化,而默認不寫的話,是公有的構造方法,外部能夠顯式的調用來建立對象,咱們的目的是讓外部不能建立對象。框架

提供獲取實例的公有方法

對外只提供一個公有的的方法,用來獲取實例,而這個實例是不是惟一的,單例的,由方法決定,外部無需關心。jvm

單例模式的常見寫法(以下,重點)

餓漢式和懶漢式的區別

餓漢式

餓漢式,從名字上也很好理解,就是「比較餓」,火燒眉毛的想吃飯,實例在初始化的時候就已經建好了,無論你有沒有用到,都先建好了再說。

懶漢式

餓漢式,從名字上也很好理解,就是「比較懶」,不想吃飯,等餓的時候再吃。在初始化的時候先不建好對象,若是以後用到了,再建立對象。

1.餓漢式(靜態變量)--可使用

A類

public class A {
    //私有的構造方法
    private A(){}
    //私有的靜態變量
    private final static A a=new A();
    //對外的公有方法
    public static A getInstance(){
        return a;
    }
}複製代碼

測試類

public class test {
    public static void main(String[] args){
        A a1=A.getInstance();
        System.out.println(a1.hashCode());

        A a2=A.getInstance();
        System.out.println(a2.hashCode());
    }
}複製代碼

運行結果


說明

該方法採用的靜態常量的方法來生成對應的實例,其只在類加載的時候就生成了,後續並不會再生成,因此其爲單例的。函數

優勢

在類加載的時候,就完成實例化,避免線程同步問題。工具

缺點

沒有達到懶加載的效果,若是從始到終都沒有用到這個實例,可能會致使內存的浪費。

2.餓漢式(靜態代碼塊)--可使用

A類

public class A { 
    //私有的構造方法
     private A(){}
    //私有的靜態變量
     private final static A a; 
    //靜態代碼塊 
    static{ a=new A(); } 
    //對外的公有方法
    public static A getInstance(){
     return a; 
    }
}複製代碼

測試類

public class test {
    public static void main(String[] args){
        A a1=A.getInstance();
        System.out.println(a1.hashCode());

        A a2=A.getInstance();
        System.out.println(a2.hashCode());
    }
}複製代碼

運行結果


說明

該靜態代碼塊的餓漢式單例模式與靜態變量的餓漢式模式大同小異,只是將初始化過程移到了靜態代碼塊中。

優勢缺點

與靜態變量餓漢式的優缺點相似。

3.懶漢式

A類

public class A {
    //私有的構造方法
    private A(){}
    //私有的靜態變量
    private  static A a;
    //對外的公有方法
    public static A getInstance(){
        if(a==null){
            a=new A();
        }
        return a;
    }
}複製代碼

測試類和運行結果

同上。

優勢

該方法的確作到了用到即加載,也就是當調用getInstance的時候,才判斷是否有該對象,若是不爲空,則直接放回,若是爲空,則新建一個對象並返回,達到了懶加載的效果。

缺點

當多線程的時候,可能會產生多個實例。好比我有兩個線程,同時調用getInstance方法,並都到了if語句,他們都新建了對象,那這裏就不是單例的啦。

4.懶漢式(線程安全,同步方法)--可使用

public class A {
    //私有的構造方法
    private A(){}
    //私有的靜態變量
    private  static A a;
    //對外的公有方法
    public synchronized static A getInstance(){
        if(a==null){
            a=new A();
        }
        return a;
    }
}複製代碼

測試類和運行結果

同上。

優勢

經過synchronize關鍵字,解決了線程不安全的問題。若是兩個線程同時調用getInstance方法時,那就先執行一個線程,另外一個等待,等第一個線程運行結束了,另外一個等待的開始執行。

缺點

這種方法是解決了線程不安全的問題,卻給性能帶來了很大的問題,效率過低了,getInstance常常發生,每一次都要同步這個方法。

咱們想着既然是方法同步致使了性能的問題,咱們核心的代碼就是新建對象的過程,也就是new A();的過程,咱們能不能只對部分代碼進行同步呢?

那就是方法5啦。

5.懶漢式(線程不安全)

A類

public class A {
    //私有的構造方法
    private A(){}
    //私有的靜態變量
    private  static A a;
    public  static A getInstance(){
        if(a==null){
            synchronized (A.class){
                a=new A();
            }
        }
        return a;
    }
} 複製代碼

測試類和運行結果

如上。

優勢

懶漢式的通用優勢,用到才建立,達到懶加載的效果。

缺點

這個沒有意義,並無解決多線程的問題。咱們能夠看到若是兩個線程同時調用getInstance方法,而且都已經進入了if語句,即synchronized的位置,即使同步了,第一個線程先執行,進入synchronized同步的代碼塊,建立了對象,另外一個進入等待狀態,等第一個線程執行結束,第二個線程仍是會進入synchronized同步的代碼塊,建立對象。這個時候咱們能夠發現,對這代碼塊加了synchronized沒有任何意義,仍是建立了多個對象,並不符合單例。

6.雙重檢查 --強烈推薦使用

A類

public class A {
    //私有的構造方法
    private A() {
    }

    //私有的靜態變量
    private volatile static A a;

    //對外的公有方法
    public static A getInstance() {
        if (a == null) {
            synchronized (A.class) {
                if (a == null) {
                    a = new A();
                }
            }
        }
        return a;
    }
} 複製代碼

測試類和運行結果

同上。

優勢

強烈推薦使用,這種寫法既避免了在多線程中出現線程不安全的狀況,也能提升性能。

咱具體來講,若是兩個線程同時調用了getInstance方法,而且都已到達了if語句以後,synchronized語句以前,此時第一個線程進入synchronized之中,先判斷是否爲空,很顯然第一次確定爲空,那麼則新建了對象。等到第二個線程進入synchronized之中,先判斷是否爲空,顯然第一個已經建立了,因此即不新建對象。下次,不論是一個線程或者多個線程,在第一個if語句那就判斷出有對象了,便直接返回啦,根本進不了裏面的代碼。

缺點

就是這麼完美,沒有缺點,哈哈哈。


volatile(插曲)

咱先來看一個概念,重排序,也就是語句的執行順序會被從新安排。其主要分爲三種:

1.編譯器優化的重排序:能夠從新安排語句的執行順序。

2.指令級並行的重排序:現代處理器採用指令級並行技術,將多條指令重疊執行。

3.內存系統的重排序:因爲處理器使用緩存和讀寫緩衝區,因此看上去多是亂序的。

上面代碼中的a = new A();可能被被JVM分解成以下代碼:

// 能夠分解爲如下三個步驟
1 memory=allocate();// 分配內存 至關於c的malloc
2 ctorInstanc(memory) //初始化對象
3 s=memory //設置s指向剛分配的地址複製代碼

// 上述三個步驟可能會被重排序爲 1-3-2,也就是:
1 memory=allocate();// 分配內存 至關於c的malloc
3 s=memory //設置s指向剛分配的地址
2 ctorInstanc(memory) //初始化對象  複製代碼

一旦假設發生了這樣的重排序,好比線程A在執行了步驟1和步驟3,可是步驟2尚未執行完。這個時候線程B有進入了第一個if語句,它會判斷a不爲空,即直接返回了a。其實這是一個未初始化完成的a,即會出現問題。

因此咱們會將入volatile關鍵字,來禁止這樣的重排序,便可正常運行。

7.靜態內部類 --強烈推薦使用

A類

public class A {
    //私有構造函數
    private A() {
    }

    //私有的靜態內部類
    private static class B {
        //私有的靜態變量
        private static A a = new A();
    }

    //對外的公有方法
    public static A getInstance() {
        return B.a;
    }
}複製代碼

優勢

B在A裝載的時候並不會裝載,而是會在調用getInstance的時候裝載,這利用了JVM的裝載機制。這樣一來,優勢有兩點,其一就是沒有A加載的時候,就裝載了a對象,而是在調用的時候才裝載,避免了資源的浪費。其二是多線程狀態下,沒有線程安全性的問題。

缺點

沒有缺點,太完美啦。

8.枚舉 --Java粑粑強烈推薦使用

問題1:私有構造器並不安全

若是不明白反射,能夠查看我以前的文章,傳送門,萬字總結之反射(框架之魂)

若是咱們的對象是經過反射方法invoke出來,這樣新建的對象與經過調用getInstance新建的對象是不同的,具體咱來看代碼。

public class test {
    public static void main(String[] args) throws Exception {
        A a=A.getInstance();
        A b=A.getInstance();
        System.out.println("a的hash:"+a.hashCode()+",b的hash:"+b.hashCode());

        Constructor<A> constructor=A.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        A c=constructor.newInstance();
        System.out.println("a的hash:"+a.hashCode()+",c的hash:"+c.hashCode());

    }
}複製代碼

咱們來看下運行結果:


咱們能夠看到c的hashcode是和a,b不同,由於c是經過構造器反射出來的,由此能夠證實私有構造器所組成的單例模式並非十分安全的。

問題2:序列化問題

咱們先將A類實現一個Serializable接口,具體代碼以下,跟以前的雙重if檢查同樣,只是多了個接口。

public class A implements Serializable {
    //私有的構造方法
    private A() {
    }

    //私有的靜態變量
    private volatile static A a;

    //對外的公有方法
    public static A getInstance() {
        if (a == null) {
            synchronized (A.class) {
                if (a == null) {
                    a = new A();
                }
            }
        }
        return a;
    }
} 複製代碼

測試類:

public class test {
    public static void main(String[] args) throws Exception {
        A s = A.getInstance();

        //寫
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("學習Java的小姐姐"));
        oos.writeObject(s);
        oos.flush();
        oos.close();
        //讀
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("學習Java的小姐姐"));
        A s1 = (A)ois.readObject();
        ois.close();

        System.out.println(s+"\n"+s1);
        System.out.println("序列化先後兩個是否同一個:"+(s==s1));
    }
}複製代碼

咱們來看下運行結果,很顯然序列化先後兩個對象並不相等。爲何會出現這種問題呢?這個講起來,又能夠寫一篇文章了。簡單來講,任何一個readObject方法,不論是顯式的仍是默認的,它都會返回一個新建的實例,這個新建的實例不一樣於該類初始化時建立的實例。


A類

public enum A {
  a;
  public A getInstance(){
      return a;
  }
}
複製代碼

看着代碼量不多,咱們將其編譯下,代碼以下:

public final class  A extends Enum< A> {        public static final  A a;        public static  A[] values();        public static  AvalueOf(String s);        static {};
}複製代碼

如何解決問題1?

public class test {
    public static void main(String[] args) throws Exception {
        A a1 = A.a;
        A a2 = A.a;
        System.out.println("正常狀況下,實例化兩個實例是否相同:" + (a1 == a2));

        Constructor<A> constructor = null;
        constructor = A.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        A a3 = null;
        a3 = constructor.newInstance();
        System.out.println("a1的hash:" + a1.hashCode() + ",a2的hash:" + a2.hashCode() + ",a3的hash:" + a3.hashCode());
        System.out.println("經過反射攻擊單例模式狀況下,實例化兩個實例是否相同:" + (a1 == a3));
    }
}複製代碼

運行結果:


咱們看到報錯了,是在尋找構造函數的時候報錯的,即沒有無參的構造方法,那咱們看下他繼承的父類ENUM有沒有構造函數,看下源碼,發現有個兩個參數String和int類型的構造方法,咱們再看下是否是構造方法的問題。


咱們再用父類的有參構造方法試下,代碼以下:

public class test {
    public static void main(String[] args) throws Exception {
        A a1 = A.a;
        A a2 = A.a;
        System.out.println("正常狀況下,實例化兩個實例是否相同:" + (a1 == a2));
        Constructor<A> constructor = null;
        constructor = A.class.getDeclaredConstructor(String.class,int.class);//其父類的構造器
        constructor.setAccessible(true);
        A a3 = null;
        a3 = constructor.newInstance("學習Java的小姐姐",1);
        System.out.println("a1的hash:" + a1.hashCode() + ",a2的hash:" + a2.hashCode() + ",a3的hash:" + a3.hashCode());
        System.out.println("經過反射攻擊單例模式狀況下,實例化兩個實例是否相同:" + (a1 == a3));
    }
}複製代碼

運行結果以下:


咱們發現報錯信息的位置已經換了,如今是已經有構造方法,而是在newInstance方法的時候報錯了,咱們跟下源碼發現,人家已經明確寫明瞭若是是枚舉類型,直接拋出異常,代碼以下,因此是沒法使用反射來操做枚舉類型的數據的。


如何解決問題2?

public class test {
    public static void main(String[] args) throws Exception {
        A s = A.a;

        //寫
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("學習Java的小姐姐"));
        oos.writeObject(s);
        oos.flush();
        oos.close();
        //讀
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("學習Java的小姐姐"));
        A s1 = (A)ois.readObject();
        ois.close();

        System.out.println(s+"\n"+s1);
        System.out.println("序列化先後兩個是否同一個:"+(s==s1));
    }
}複製代碼

運行結果;


優勢

避免了反射帶來的對象不一致問題和反序列問題,簡單來講,就是簡單高效沒問題。

結語

看到這裏的都是真愛的,在這裏先謝謝各位大佬啦。

單例模式是最簡單的一種設計模式,主要包括八種形式,分別是餓漢式靜態變量,餓漢式靜態代碼塊,懶漢式線程不安全,懶漢式線程安全,懶漢式線程不安全(沒啥意義),懶漢式雙重否認線程安全,內部靜態類,枚舉類型。

這幾種最優的是枚舉類型和內部靜態類,其次是懶漢式雙重否認,剩下的都差很少啦。

若是有說的不對的地方,還請各位指正,我好繼續學習去。

求個關注

小姐姐陪你學習,陪你走心。


參考資料

一個單例模式中volatile關鍵字引起的思考

相關文章
相關標籤/搜索