設計模式之單例模式(懶漢式單例、餓漢式單例、登記式單例)

單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種類型的設計模式屬於建立型模式,它提供了一種建立對象的最佳方式html

這種模式涉及到一個單一的類,該類負責建立本身的對象,同時確保只有單個對象被建立。這個類提供了一種訪問其惟一的對象的方式,能夠直接訪問,不須要實例化該類的對象。android

注意:數據庫

  • 一、單例類只能有一個實例
  • 二、單例類必須本身建立本身的惟一實例。
  • 三、單例類必須給全部其餘對象提供這一實例。

介紹

意圖:保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。設計模式

主要解決:一個全局使用的類頻繁地建立與銷燬。緩存

什麼時候使用:當您想控制實例數目節省系統資源的時候。安全

如何解決:判斷系統是否已經有這個單例,若是有則返回,若是沒有則建立。多線程

關鍵代碼:構造函數是私有的。併發

應用實例:函數

  • 一、一個班級只有一個班主任。
  • 二、Windows 是多進程多線程的,在操做一個文件的時候,就不可避免地出現多個進程或線程同時操做一個文件的現象,因此全部文件的處理必須經過惟一的實例來進行。
  • 三、一些設備管理器經常設計爲單例模式,好比一個電腦有兩臺打印機,在輸出的時候就要處理不能兩臺打印機打印同一個文件。

優勢:性能

  • 一、在內存裏只有一個實例減小了內存的開銷,尤爲是頻繁的建立和銷燬實例(好比管理學院首頁頁面緩存)。
  • 二、避免對資源的多重佔用(好比寫文件操做)。

缺點:沒有接口不能繼承,與單一職責原則衝突,一個類應該只關心內部邏輯,而不關心外面怎麼樣來實例化。

使用場景:

  • 一、要求生產惟一序列號
  • 二、WEB 中的計數器,不用每次刷新都在數據庫里加一次,用單例先緩存起來。
  • 三、建立的一個對象須要消耗的資源過多,好比 I/O數據庫的鏈接等。

注意事項:getInstance() 方法中須要使用同步鎖 synchronized (Singleton.class) 防止多線程同時進入形成 instance 被屢次實例化。

 

單例模式的寫法有好幾種,這裏主要介紹三種:懶漢式單例餓漢式單例、登記式單例

1、懶漢式單例

//懶漢式單例類.在第一次調用的時候實例化本身   
public class Singleton {  
    private Singleton() {}  
    private static Singleton single=null;  
    //靜態工廠方法   
    public static Singleton getInstance() {  
         if (single == null) {    
             single = new Singleton();  
         }    
        return single;  
    }  
}
Singleton經過將構造方法限定爲private避免了類在外部被實例化,在同一個虛擬機範圍內, Singleton的惟一實例只能經過getInstance()方法訪問

(事實上,經過Java反射機制是可以實例化構造方法爲private的類的,那基本上會使全部的Java單例實現失效。此問題在此處不作討論,姑且掩耳盜鈴地認爲反射機制不存在。)

可是以上懶漢式單例的實現沒有考慮線程安全問題,它是線程不安全的,併發環境下極可能出現多個Singleton實例,要實現線程安全,有如下三種方式,都是對getInstance這個方法改造,保證了懶漢式單例的線程安全,若是你第一次接觸單例模式,對線程安全不是很瞭解,能夠先跳過下面這三小條,去看餓漢式單例,等看完後面再回頭考慮線程安全的問題:

一、在getInstance方法上加同步

public static synchronized Singleton getInstance() {  
    if (single == null) {    
        single = new Singleton();  
    }    
    return single;  
}

二、雙重檢查鎖定

public static Singleton getInstance() {  
    if (singleton == null) {    
        synchronized (Singleton.class) {    
           if (singleton == null) {    
              singleton = new Singleton();   
           }    
        }    
    }    
    return singleton;   
}

三、靜態內部類

public class Singleton {    
    private static class LazyHolder {    
       private static final Singleton INSTANCE = new Singleton();    
    }    
    private Singleton (){}    
    public static final Singleton getInstance() {    
       return LazyHolder.INSTANCE;    
    }    
}

這種比上面一、2都好一些,既實現了線程安全,又避免了同步帶來的性能影響。

2、餓漢式單例

//餓漢式單例類.在類初始化時,已經自行實例化   
public class Singleton1 {  
    private Singleton1() {}  
    private static final Singleton1 single = new Singleton1();  
    //靜態工廠方法   
    public static Singleton1 getInstance() {  
        return single;  
    }  
}

餓漢式在類建立的同時就已經建立好一個靜態的對象供系統使用,之後再也不改變,因此天生是線程安全的。

3、登記式單例(可忽略)

//相似Spring裏面的方法,將類名註冊,下次從裏面直接獲取。  
public class Singleton3 {  
    private static Map<String,Singleton3> map = new HashMap<String,Singleton3>();  
    static{  
        Singleton3 single = new Singleton3();  
        map.put(single.getClass().getName(), single);  
    }  
    //保護的默認構造子  
    protected Singleton3(){}  
    //靜態工廠方法,返還此類唯一的實例  
    public static Singleton3 getInstance(String name) {  
        if(name == null) {  
            name = Singleton3.class.getName();  
            System.out.println("name == null"+"--->name="+name);  
        }  
        if(map.get(name) == null) {  
            try {  
                map.put(name, (Singleton3) Class.forName(name).newInstance());  
            } catch (InstantiationException e) {  
                e.printStackTrace();  
            } catch (IllegalAccessException e) {  
                e.printStackTrace();  
            } catch (ClassNotFoundException e) {  
                e.printStackTrace();  
            }  
        }  
        return map.get(name);  
    }  
    //一個示意性的商業方法  
    public String about() {      
        return "Hello, I am RegSingleton.";      
    }      
    public static void main(String[] args) {  
        Singleton3 single3 = Singleton3.getInstance(null);  
        System.out.println(single3.about());  
    }  
}

登記式單例實際上維護了一組單例類的實例,將這些實例存放在一個Map(登記薄)中,對於已經登記過的實例,則從Map直接返回,對於沒有登記的,則先登記,而後返回。 

這裏我對登記式單例標記了可忽略,個人理解來講,首先它用的比較少,另外其實內部實現仍是用的餓漢式單例,由於其中的static方法塊,它的單例在類被裝載的時候就被實例化了。

餓漢式和懶漢式區別

從名字上來講,餓漢和懶漢,

餓漢就是類一旦加載,就把單例初始化完成,保證getInstance的時候,單例是已經存在的了,

而懶漢比較懶,只有當調用getInstance的時候,纔回去初始化這個單例。

另外從如下兩點再區分如下這兩種方式:

一、線程安全:

餓漢式天生就是線程安全的,能夠直接用於多線程而不會出現問題,

懶漢式自己是非線程安全的,爲了實現線程安全有幾種寫法,分別是上面的一、二、3,這三種實如今資源加載和性能方面有些區別。

 

二、資源加載和性能:

餓漢式在類建立的同時就實例化一個靜態對象出來,無論以後會不會使用這個單例,都會佔據必定的內存,可是相應的,在第一次調用時速度也會更快,由於其資源已經初始化完成,

而懶漢式顧名思義,會延遲加載,在第一次使用該單例的時候纔會實例化對象出來,第一次調用時要作初始化,若是要作的工做比較多,性能上會有些延遲,以後就和餓漢式同樣了。

至於一、二、3這三種實現又有些區別,

第1種,在方法調用上加了同步,雖然線程安全了,可是每次都要同步,會影響性能,畢竟99%的狀況下是不須要同步的,

第2種,在getInstance中作了兩次null檢查,確保了只有第一次調用單例的時候纔會作同步,這樣也是線程安全的,同時避免了每次都同步的性能損耗

第3種,利用了classloader的機制來保證初始化instance時只有一個線程,因此也是線程安全的,同時沒有性能損耗,因此通常我傾向於使用這一種。

什麼是線程安全?

若是你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。若是每次運行結果和單線程運行的結果是同樣的,並且其餘的變量的值也和預期的是同樣的,就是線程安全的。

或者說:一個類或者程序所提供的接口對於線程來講是原子操做,或者多個線程之間的切換不會致使該接口的執行結果存在二義性,也就是說咱們不用考慮同步的問題,那就是線程安全的。

應用

如下是一個單例類使用的例子,以懶漢式爲例,這裏爲了保證線程安全,使用了雙重檢查鎖定的方式:

public class TestSingleton {  
    String name = null;  
  
        private TestSingleton() {  
    }  
  
    private static volatile TestSingleton instance = null;  
  
    public static TestSingleton getInstance() {  
           if (instance == null) {    
             synchronized (TestSingleton.class) {    
                if (instance == null) {    
                   instance = new TestSingleton();   
                }    
             }    
           }   
           return instance;  
    }  
  
    public String getName() {  
        return name;  
    }  
  
    public void setName(String name) {  
        this.name = name;  
    }  
  
    public void printInfo() {  
        System.out.println("the name is " + name);  
    }  
  
}

能夠看到裏面加了volatile關鍵字來聲明單例對象,既然synchronized已經起到了多線程下原子性、有序性、可見性的做用,爲何還要加volatile呢?

public class TMain {  
    public static void main(String[] args){  
        TestStream ts1 = TestSingleton.getInstance();  
        ts1.setName("jason");  
        TestStream ts2 = TestSingleton.getInstance();  
        ts2.setName("0539");  
          
        ts1.printInfo();  
        ts2.printInfo();  
          
        if(ts1 == ts2){  
            System.out.println("建立的是同一個實例");  
        }else{  
            System.out.println("建立的不是同一個實例");  
        }  
    }  
}

運行結果:

the name is 0539
the name is 0539
建立的是同一個實例

結論:由結果能夠得知單例模式爲一個面向對象的應用程序提供了對象唯一的訪問點,無論它實現何種功能,整個應用程序都會同享一個實例對象

對於單例模式的幾種實現方式,知道餓漢式和懶漢式的區別,線程安全,資源加載的時機,還有懶漢式爲了實現線程安全的3種方式的細微差異。

優勢:

(1)因爲單例模式在內存中只有一個實例減小了內存開支,特別是一個對象須要頻繁地建立、銷燬時,並且建立或銷燬時性能又沒法優化,單例模式的優點就很是明顯。 
(2)因爲單例模式只生成一個實例,因此減小了系統的性能開銷,當一個對象的產生須要比較多的資源時,好比讀取配置、產生其餘依賴對象時,則能夠經過在應用啓動時直接產生一個單例對象,而後用永久駐留內存的方式來解決。 
(3)單例模式能夠避免對資源的多重佔用,例如一個寫文件操做,因爲只有一個實例存在內存中,避免對同一個資源文件的同時寫操做。 
(4)單例模式能夠在系統設置全局的訪問點,優化和共享資源訪問,例如,能夠設計一個單例類負責全部數據表的映射處理

 

缺點:

(1)單例模式通常沒有接口擴展很困難,若要擴展,除了修改代碼基本上沒有第二種途徑能夠實現。 
(2)單例對象若是持有Context,那麼很容易引起內存泄露,此時須要注意傳給單例對象的Context最好是Application Context

 

ending...

參考博文:

https://www.runoob.com/design-pattern/singleton-pattern.html

https://www.cnblogs.com/crazy-wang-android/p/9054771.html

相關文章
相關標籤/搜索