單例模式(含多線程處理)

版權聲明:本文爲博主原創文章,轉載請註明出處,歡迎交流學習!數據庫

      單例,顧名思義一個類只有一個實例。爲何要使用單例模式,或者說什麼樣的類能夠作成單例的?在工做中我發現,使用單例模式的類都有一個共同點,那就是這個類沒有狀態,也就是說不管你實例化多少個對象,其實都是同樣的。又或者是一個類須要頻繁實例化而後銷燬對象。還有很重要的一點,若是這個類有多個實例的話,會產生程序錯誤或者不符合業務邏輯。這種狀況下,若是咱們不把類作成單例,程序中就會存在多個如出一轍的實例,這樣會形成內存資源的浪費,並且容易產生程序錯誤。總結一下,判斷一個類是否要作成單例,最簡單的一點就是,若是這個類有多個實例會產生錯誤,或者在整個應用程序中,共享一份資源。安全

     在實際開發中,一些資源管理器、數據庫鏈接等經常設計成單例模式,避免實例重複建立。實現單例有幾種經常使用的方式,下面咱們來探討一下他們各自的優劣。多線程

     第一種方式:懶漢式單例併發

     

 1 public class Singleton {
 2     //一個靜態實例
 3     private static Singleton singleton;
 4     //私有構造方法
 5     private Singleton(){
 6         
 7     }
 8     //提供一個公共靜態方法來獲取一個實例
 9     public static Singleton getInstance(){
10         
11         if(singleton == null ){
12             
13             singleton = new Singleton();
14         }
15         
16         return singleton;
17         
18     }
19 }

 

    在不考慮併發的狀況下,這是標準的單例構造方式,它經過如下幾個要點來保證咱們得到的實例是單一的。ide

    一、靜態實例,靜態的屬性在內存中是惟一的;
性能

    二、私有的構造方法,這就保證了不能人爲的去調用構造方法來生成一個實例;學習

    三、提供公共的靜態方法來返回一個實例, 把這個方法設置爲靜態的是有緣由的,由於這樣咱們能夠經過類名來直接調用此方法(此時咱們尚未得到實例,沒法經過實例來調用方法),而非靜態的方法必須經過實例來調用,所以這裏咱們要把它聲明爲靜態的方法經過類名來調用;測試

    四、判斷只有持有的靜態實例爲null時才經過構造方法產生一個實例,不然直接返回。優化

    在多線程環境下,這種方式是不安全,經過本身的測試,多個線程同時訪問它可能生成不止一個實例,咱們經過程序來驗證這個問題:spa

    

 1 public class Singleton {
 2     //一個靜態實例
 3     private static Singleton singleton;
 4     //私有構造方法
 5     private Singleton(){
 6         
 7     }
 8     //提供一個公共靜態方法來獲取一個實例
 9     public static Singleton getInstance(){
10         
11         if(singleton == null ){
12             
13             try {
14                 Thread.sleep(5000);  //模擬線程在這裏發生阻塞
15             } catch (InterruptedException e) {
16                 e.printStackTrace();
17             }
18             
19             singleton = new Singleton();
20         }
21         
22         return singleton;
23         
24     }
25 }

    測試類:

public class TestSingleton {
    
    public static void main(String[] args) {
        
        Thread t1 = new MyThread();
        Thread t2 = new MyThread();
        
        t1.start();
        t2.start();
    }

}


class MyThread extends Thread{
    
    @Override
    public void run() {
        
        System.out.println(Singleton.getInstance()); //打印生成的實例,會輸出實例的類名+哈希碼值
        
    }
}

    執行該測試類,輸出的結果以下:

    

    從以上結果能夠看出,輸出兩個實例而且實例的hashcode值不相同,證實了咱們得到了兩個不同的實例。這是什麼緣由呢?咱們生成了兩個線程同時訪問getInstance()方法,在程序中我讓線程睡眠了5秒,是爲了模擬線程在此處發生阻塞,當第一個線程t1進入getInstance()方法,判斷完singleton爲null,接着進入if語句準備建立實例,同時在t1建立實例以前,另外一個線程t2也進入getInstance()方法,此時判斷singleton也爲null,所以線程t2也會進入if語句準備建立實例,這樣問題就來了,有兩個線程都進入了if語句建立實例,這樣就產生了兩個實例。

    爲了不這個問題,在多線程狀況下咱們要考慮線程同步問題了,最簡單的方式固然是下面這種方式,直接讓整個方法同步:

    

public class Singleton {
    //一個靜態實例
    private static Singleton singleton;
    //私有構造方法
    private Singleton(){
        
    }
    //提供一個公共靜態方法來獲取一個實例
    public static synchronized Singleton getInstance(){
        
        if(singleton == null ){
            
            try {
                Thread.sleep(5000);  //模擬線程在這裏發生阻塞
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            singleton = new Singleton();
        }
        
        return singleton;
        
    }
}

    咱們經過給getInstance()方法加synchronized關鍵字來讓整個方法同步,咱們一樣能夠執行上面給出的測試類來進行測試,打印結果以下:

    

    從測試結果能夠看出,兩次調用getInstance()方法返回的是同一個實例,這就達到了咱們單例的目的。這種方式雖然解決了多線程同步問題,可是並不推薦採用這種設計,由於沒有必要對整個方法進行同步,這樣會大大增長線程等待的時間,下降程序的性能。咱們須要對這種設計進行優化,這就是咱們下面要討論的第二種實現方式。

    第二種方式:雙重校驗鎖

    因爲對整個方法加鎖的設計效率過低,咱們對這種方式進行優化:

    

 1 public class Singleton {
 2     //一個靜態實例
 3     private static Singleton singleton;
 4     //私有構造方法
 5     private Singleton(){
 6         
 7     }
 8     //提供一個公共靜態方法來獲取一個實例
 9     public static Singleton getInstance(){
10         
11         if(singleton == null ){
12             
13             synchronized(Singleton.class){
14                 
15                 if(singleton == null){
16                     
17                     singleton = new Singleton();
18                     
19                 }
20             }
21         }
22         
23         return singleton;
24         
25     }
26 }

    跟上面那種糟糕的設計相比,這種方式就好太多了。由於這裏只有當singleton爲null時才進行同步,當實例已經存在時直接返回,這樣就節省了無謂的等待時間,提升了效率。注意在同步塊中,咱們再次判斷了singleton是否爲空,下面解釋下爲何要這麼作。假設咱們去掉這個判斷條件,有這樣一種狀況,當兩個線程同時進入if語句,第一個線程t1得到線程鎖執行實例建立語句並返回一個實例,接着第二個線程t2得到線程鎖,若是這裏沒有實例是否爲空的判斷條件,t2也會執行下面的語句返回另外一個實例,這樣就產生了多個實例。所以這裏必需要判斷實例是否爲空,若是已經存在就直接返回,不會再去建立實例了。這種方式既保證了線程安全,也改善了程序的執行效率。

    第三種方式:靜態內部類

    

 1 public class Singleton {
 2     //靜態內部類
 3     private static class SingletonHolder{
 4         private static Singleton singleton = new Singleton();
 5     }
 6     //私有構造方法
 7     private Singleton(){
 8         
 9     }
10     //提供一個公共靜態方法來獲取一個實例
11     public static Singleton getInstance(){
12         
13         return SingletonHolder.singleton;
14         
15     }
16 }

    這種方式利用了JVM的類加載機制,保證了多線程環境下只會生成一個實例。當某個線程訪問getInstance()方法時,執行語句訪問內部類SingletonHolder的靜態屬性singleton,這也就是說當前類主動使用了改靜態屬性,JVM會加載內部類並初始化內部類的靜態屬性singleton,在這個初始化過程當中,其餘的線程是沒法訪問該靜態變量的,這是JVM內部幫咱們作的同步,咱們無須擔憂多線程問題,而且這個靜態屬性只會初始化一次,所以singleton是單例的。

    第四種方式:餓漢式

    

 1 public class Singleton {
 2     //一個靜態實例
 3     private static Singleton singleton = new Singleton();
 4     //私有構造方法
 5     private Singleton(){
 6         
 7     }
 8     //提供一個公共靜態方法來獲取一個實例
 9     public static Singleton getInstance(){
10         
11         return singleton;
12         
13     }
14 }

    這種方式也是利用了JVM的類加載機制,在單例類被加載時就初始化一個靜態實例,所以這種方式也是線程安全的。這種方式存在的問題就是,一旦Singleton類被加載就會產生一個靜態實例,而類被加載的緣由有不少種,事實上咱們可能從始至終都沒有使用這個實例,這樣會形成內存的浪費。在實際開發中,這個問題影響不大。

    以上內容介紹了幾種常見的單例模式的實現方式,分析了在多線程狀況下的處理方式, 在工做中可根據實際須要選擇合適的實現方式。還有一種利用枚舉來實現單例的方式,在工做中不多有人這樣寫過,不作探討。

相關文章
相關標籤/搜索