8種單例模式寫法助你搞定面試

1. 單例模式常見問題

爲何要有單例模式java

單例模式是一種設計模式,它限制了實例化一個對象的行爲,始終至多隻有一個實例。當只須要一個對象來協調整個系統的操做時,這種模式就很是有用.它描述瞭如何解決重複出現的設計問題, 好比咱們項目中的配置工具類,日誌工具類等等。設計模式

如何設計單例模式 ?安全

1.單例類如何控制其實例化bash

2.如何確保只有一個實例多線程

經過一下措施解決這些問題:併發

private構造函數,類的實例話不對外開放,由本身內部來完成這個操做,確保永遠不會從類外部實例化類,避免外部隨意new出來新的實例。函數

該實例一般存儲爲私有靜態變量,提供一個靜態方法,返回對實例的引用。若是是在多線程環境下則用鎖或者內部類來解決線程安全性問題。工具

2. 單例類有哪些特色 ?

私有構造函數
它將阻止從類外部實例化新對象性能

它應該只有一個實例
這是經過在類中提供實例來方法完成的,阻止外部類或子類來建立實例。這是經過在java中使構造函數私有來完成的,這樣任何類都不能訪問構造函數,所以沒法實例化它。ui

單實例應該是全局可訪問的
單例類的實例應該是全局可訪問的,以便每一個類均可以使用它。在Java中,它是經過使實例的訪問說明符爲public來完成的。

節省內存,減小GC

由於是全局至多隻有一個實例,避免了處處new對象,形成浪費內存,以及GC,有了單例模式能夠避免這些問題

3. 單例模式8種寫法

下面由我給你們介紹8種單例模式的寫法,各有千秋,存在即合理,經過本身的使用場景選一款使用便可。咱們選擇單例模式時的挑選標準或者說評估一種單例模式寫法的優劣時一般會根據一下兩種因素來衡量:

1.在多線程環境下行爲是否線程安全

2.餓漢以及懶漢

3.編碼是否優雅(理解起來是否比較直觀)

1. 餓漢式線程安全的

public class SingleTon{

	private static final SingleTon INSTANCE = new SingleTon();

	private SingleTon(){ }

	public static SingleTon getInstance(){
		return INSTANCE;
	}
	public static void main(String[] args) {
        SingleTon instance1 = SingleTon.getInstance();
        SingleTon instance2 = SingleTon.getInstance();
        System.out.println(instance1 == instance2);
    }

}
複製代碼

這種寫法是很是簡單實用的,值得推薦,惟一缺點就是懶漢式的,也就是說不論是否須要用到這個方法,當類加載的時候都會生成一個對象。
除此以外,這種寫法是線程安全的。類加載到內存後,就實例化一個單例,JVM保證線程安全,

2. 餓漢式線程安全(變種寫法)

public class SingleTon{

	private static final SingleTon INSTANCE ;

	static {
	    INSTANCE = new SingleTon(); 
	}

	private SingleTon(){}

	public static SingleTon getInstance(){
		return INSTANCE;
	}
        public static void main(String[] args) {
        SingleTon instance1 = SingleTon.getInstance();
        SingleTon instance2 = SingleTon.getInstance();
        System.out.println(instance1 == instance2);
    }

}
複製代碼

3. 懶漢式線程不安全

public class SingleTon{

	private static  SingleTon instance ;

	private SingleTon(){}

	public static SingleTon getInstance(){
            if(instance == null){
                instance = new SingleTon();
            }
            return instance;
	}

	public static void main(String[] args) {
        SingleTon instance1 = SingleTon.getInstance();
        SingleTon instance2 = SingleTon.getInstance();
        System.out.println(instance1 == instance2);
        
        // 經過開啓100個線程 比較是不是相同對象
        for(int i=0;i<100;i++){
             new Thread(()->
                System.out.println(SingleTon.getInstance().hashCode())
            ).start();
        }
        
    }

}
複製代碼

這種寫法雖然達到了按需初始化的目的,但卻帶來線程不安全的問題,至於爲何在併發狀況下上述的例子是不安全的呢 ?

// 經過開啓100個線程 比較是不是相同對象
        for(int i=0;i<100;i++){
             new Thread(()->
                System.out.println(SingleTon.getInstance().hashCode())
            ).start();
        }
複製代碼

爲了使效果更直觀一點咱們對getInstance 方法稍作修改,每一個線程進入以後休眠一毫秒,這樣作的目的是爲了每一個線程都儘量得到cpu時間片去執行。代碼以下

public static SingleTon getInstance(){
       if(instance == null){
           try {
               Thread.sleep(1);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           instance = new SingleTon();
       }
   	return instance;
   }
複製代碼

執行結果以下

上述的單例寫法,咱們是能夠創造出多個實例的,至於爲何在這裏要稍微解釋一下,這裏涉及了同步問題

形成線程不安全的緣由:

當併發訪問的時候,第一個調用getInstance方法的線程t1,在判斷完singleton是null的時候,線程A就進入了if塊準備創造實例,可是同時另一個線程B在線程A還未創造出實例以前,就又進行了singleton是否爲null的判斷,這時singleton依然爲null,因此線程B也會進入if塊去創造實例,這時問題就出來了,有兩個線程都進入了if塊去創造實例,結果就形成單例模式並不是單例。
注:這裏經過休眠一毫秒來模擬線程掛起,爲初始化完instance


爲了解決這個問題,咱們能夠採起加鎖措施,因此有了下面這種寫法

4. 懶漢式線程安全(粗粒度Synchronized)

public class SingleTon{

	private static  SingleTon instance ;

	private SingleTon(){}

	public static SingleTon synchronized getInstance(){
	    if(instance == null){
                instance = new SingleTon();
	    }
	    return instance;
	}

	public static void main(String[] args) {
	    SingleTon instance1 = SingleTon.getInstance();
	    SingleTon instance2 = SingleTon.getInstance();
	    System.out.println(instance1 == instance2);
            // 經過開啓100個線程 比較是不是相同對象
            for(int i=0;i<100;i++){
                new Thread(()->
                System.out.println(SingleTon.getInstance().hashCode())
            ).start();
        }
        
    }

}
複製代碼

因爲第三種方式出現了線程不安全的問題,因此對getInstance方法加了synchronized來保證多線程環境下的線程安全性問題,這種作法雖解決了多線程問題可是效率比較低。 由於鎖住了整個方法,其餘進入的現成都只能阻塞等待了,這樣會形成不少無謂的等待。

因而可能有人會想到可不可讓鎖的粒度更細一點,只鎖住相關代碼塊能否?因此有了第五種寫法。

5. 懶漢式線程不安全(synchronized代碼塊)

public class SingleTon{

	private static  SingleTon instance ;

	private SingleTon(){}

	public static SingleTon getInstance(){
	    if(insatnce == null){
	        synchronied(SingleTon.class){
                    instance = new SingleTon();
	        }
	    }
	    return instance;
	}

	public static void main(String[] args) {
        SingleTon instance1 = SingleTon.getInstance();
        SingleTon instance2 = SingleTon.getInstance();
        System.out.println(instance1 == instance2);
        
        // 經過開啓100個線程 比較是不是相同對象
        for(int i=0;i<100;i++){
             new Thread(()->
                System.out.println(SingleTon.getInstance().hashCode())
            ).start();
        }
        
    }

}
複製代碼

當併發訪問的時候,第一個調用getInstance方法的線程t1,在判斷完instance是null的時候,線程A就進入了if塊而且持有了synchronized鎖,可是同時另一個線程t2在線程t1還未創造出實例以前,就又進行了instance是否爲null的判斷,這時instance依然爲null,因此線程t2也會進入if塊去創造實例,他會在synchronized代碼外面阻塞等待,直到t1釋放鎖,這時問題就出來了,有兩個線程都實例化了新的對象。

形成這個問題的緣由就是線程進入了if塊而且在等待synchronized鎖的過程當中有可能上一個線程已經建立了實例,因此進入synchronized代碼塊以後還須要在判斷一次,因而有了下面這種雙重檢驗鎖的寫法。

6. 懶漢式線程安全(雙重檢驗加鎖)

public class SingleTon{

	private static  volatile SingleTon instance ;

	private SingleTon(){}

	public static SingleTon getInstance(){
	    if(instance == null){
	        synchronied(SingleTon.class){
                    if(instance == null){
                        instance = new SingleTon();
                    }
	        }
	    }
	    return instance;
	}

	public static void main(String[] args) {
        SingleTon instance1 = SingleTon.getInstance();
        SingleTon instance2 = SingleTon.getInstance();
        System.out.println(instance1 == instance2);
        
        // 經過開啓100個線程 比較是不是相同對象
        for(int i=0;i<100;i++){
             new Thread(()->
                System.out.println(SingleTon.getInstance().hashCode())
            ).start();
        }
        
    }

}
複製代碼

這種寫法基本趨於完美了,可是可能須要對一下幾點須要進行解釋:

  • 第一個判空(外層)的做用 ?
  • 第二個判空(內層)的做用 ?
  • 爲何變量修飾爲volatile ?

第一個判空(外層)的做用

首先,思考一下可不能夠去掉最外層的判斷? 答案是:能夠

其實仔細觀察以後會發現最外層的判斷跟可否線程安全正確生成單例無關!!!

它的做用是避免每次進來都要加鎖或者等待鎖,有了同步代碼塊以外的判斷以後省了不少事,當咱們的單例類實例化一個單例以後其餘後續的全部請求都不必在進入同步代碼塊繼續往下執行了,直接返回咱們曾生成的實例便可,也就是實例還未建立時才進行同步,不然就直接返回,這樣就節省了不少無謂的線程等待時間,因此最外的判斷能夠認爲是對提高性能有幫助。

第二個判空(內層)的做用

假設咱們去掉同步塊中的是否爲null的判斷,有這樣一種狀況,A線程和B線程都在同步塊外面判斷了instance爲null,結果t1線程首先得到了線程鎖,進入了同步塊,而後t1線程會創造一個實例,此時instance已經被賦予了實例,t1線程退出同步塊,直接返回了第一個創造的實例,此時t2線程得到線程鎖,也進入同步塊,此時t1線程其實已經創造好了實例,t2線程正常狀況應該直接返回的,可是由於同步塊裏沒有判斷是否爲null,直接就是一條建立實例的語句,因此t2線程也會創造一個實例返回,此時就形成創造了多個實例的狀況。

爲何變量修飾爲volatile

由於虛擬機在執行建立實例的這一步操做的時候,實際上是分了好幾步去進行的,也就是說建立一個新的對象並不是是原子性操做。在有些JVM中上述作法是沒有問題的,可是有些狀況下是會形成莫名的錯誤。

首先要明白在JVM建立新的對象時,主要要通過三步。

1.分配內存

2.初始化構造器

3.將對象指向分配的內存的地址

由於僅僅一個new 新實例的操做就涉及三個子操做,因此生成對象的操做不是原子操做

而實際狀況是,JVM會對以上三個指令進行調優,其中有一項就是調整指令的執行順序(該操做由JIT編譯器來完成)。

因此,在指令被排序的狀況下可能會出現問題,假如 2和3的步驟是相反的,先將分配好的內存地址指給instance,而後再進行初始化構造器,這時候後面的線程去請求getInstance方法時,會認爲instance對象已經實例化了,直接返回一個引用。

若是這時還沒進行構造器初始化而且這個線程使用了instance的話,則會出現線程會指向一個未初始化構造器的對象現象,從而發生錯誤。

7. 靜態內部類的方式(基本完美了)

public class SingleTon{

	public static SingleTon getInstance(){
	    return StaticSingleTon.instance;
	}
	private static class StaticSingleTon{
            private static final SingleTon instance = new SingleTon();
	}
	public static void main(String[] args) {
        SingleTon instance1 = SingleTon.getInstance();
        SingleTon instance2 = SingleTon.getInstance();
        System.out.println(instance1 == instance2);
        
        // 經過開啓100個線程 比較是不是相同對象
        for(int i=0;i<100;i++){
             new Thread(()->
                System.out.println(SingleTon.getInstance().hashCode())
            ).start();
        }
        
    }

}
複製代碼
  • 由於一個類的靜態屬性只會在第一次加載類時初始化,這是JVM幫咱們保證的,因此咱們無需擔憂併發訪問的問題。因此在初始化進行一半的時候,別的線程是沒法使用的,由於JVM會幫咱們強行同步這個過程。
  • 另外因爲靜態變量只初始化一次,因此singleton仍然是單例的。

8. 枚舉類型的單例模式(太完美以致於。。。)

public Enum SingleTon{
    
    INSTANCE;
    public static void main(String[] args) {
         // 經過開啓100個線程 比較是不是相同對象
        for(int i=0;i<100;i++){
            new Thread(()->
                System.out.println(SingleTon.getInstance().hashCode())
            ).start();
        }
        
    }

}
複製代碼

這種寫法從語法上看來是完美的,他解決了上面7種寫法都有的問題,就是咱們能夠經過反射能夠生成新的實例
可是枚舉的這種寫法是沒法經過反射來生成新的實例,由於枚舉沒有public構造方法

相關文章
相關標籤/搜索