只生成一個實例的模式,咱們稱之爲 單例模式。javascript
原文地址:單例模式 - 只有一個實例
博客地址:blog.720ui.com/java
程序在運行的時候,一般會有不少的實例。例如,咱們建立 100 個字符串的時候,會生成 100 個 String 類的實例。git
可是,有的時候,咱們只想要類的實例只存在一個。例如,「你猜我畫」中的畫板,在一個房間中的用戶須要共用一個畫板實例,而不是每一個用戶都分配一個畫板的實例。github
此外,對於數據庫鏈接、線程池、配置文件解析加載等一些很是耗時,佔用系統資源的操做,而且還存在頻繁建立和銷燬對象,若是每次都建立一個實例,這個系統開銷是很是恐怖的,因此,咱們能夠始終使用一個公共的實例,以節約系統開銷。數據庫
像這樣確保只生成一個實例的模式,咱們稱之爲 單例模式。設計模式
單例模式的目的在於,一個類只有一個實例存在,即保證一個類在內存中的對象惟一性。 安全
如今,咱們來理解這個類圖。微信
Singleton 類定義的靜態的 instance 成員變量,並將其初始化爲 Singleton 類的實例。這樣,就能夠保證單例類只有一個實例。多線程
Singleton 類的構造方法是私有的,這個設計的目的在於,防止類外部調用該構造方法。單例模式必需要確保在任何狀況下,都只能生成一個實例。爲了達到這個目的,必須設置構造方法爲私有的。換句話說,Singleton 類必須本身建立本身的惟一實例。併發
構造方法是私有的,那麼,咱們須要提供一個訪問 Singleton 類實例的全局訪問方法。
保證一個類只有一個實例,並提供一個訪問它的全局訪問方法。
顧名思義,類一加載對象就建立單例對象。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}複製代碼
值得注意的是,在定義靜態變量的時候實例化 Singleton 類,所以在類加載的時候就能夠建立了單例對象。
此時,咱們調用兩次 Singleton 類的 getInstance() 方法來獲取 Singleton 的實例。咱們發現 s1 和 s2 是同一個對象。
public class SingletonTest {
@Test
public void getInstance(){
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println("實例對象1:" + s1.hashCode());
System.out.println("實例對象2:" + s2.hashCode());
if (s1 == s2) {
System.out.println("實例相等");
} else {
System.out.println("實例不等");
}
}
}複製代碼
懶漢式,即延遲加載。單例在第一次調用 getInstance() 方法時才實例化,在類加載時並不自動實例化,在須要的時候再進行加載實例。
public class Singleton2 {
private Singleton2(){}
private static Singleton2 instance = null;
public static Singleton2 getInstance(){
if(instance == null){
instance = new Singleton2();
}
return instance;
}
}複製代碼
在多線程中,若是使用懶漢式的方式建立單例對象,那就可能會出現建立多個實例的狀況。
爲了不多個線程同時調用 getInstance() 方法,咱們可使用關鍵字 synchronized 進行線程鎖,以處理多個線程同時訪問的問題。每一個類實例對應一個線程鎖, synchronized 修飾的方法必須得到調用該方法的類實例的鎖方能執行, 不然所屬線程阻塞。方法一旦執行, 就獨佔該鎖,直到從該方法返回時纔將鎖釋放。此後被阻塞的線程方能得到該鎖, 從新進入可執行狀態。
public class Singleton3 {
private Singleton3(){}
private static Singleton3 instance = null;
public static synchronized Singleton3 getInstance(){
if(instance == null){
instance = new Singleton3();
}
return instance;
}
}複製代碼
上面的案例,在多線程中很好的工做並且是線程安全的,可是每次調用 getInstance() 方法都須要進行線程鎖定判斷,在多線程高併發訪問環境中,將會致使系統性能降低。事實上,不只效率很低,99%狀況下不須要線程鎖定判斷。
這個時候,咱們能夠經過雙重校驗鎖的方式進行處理。換句話說,利用雙重校驗鎖,第一次檢查是否實例已經建立,若是還沒建立,再進行同步的方式建立單例對象。
public class Singleton4 {
private Singleton4(){}
private static Singleton4 instance = null;
public static Singleton4 getInstance(){
if(instance == null){
synchronized(Singleton4.class){
if(instance == null){
instance = new Singleton4();
}
}
}
return instance;
}
}複製代碼
枚舉的特色是,構造方法是 private 修飾的,而且成員對象實例都是預約義的,所以咱們經過枚舉來實現單例模式很是的便捷。
public enum SingletonEnum {
INSTANCE;
private SingletonEnum(){}
}複製代碼
類加載的時候並不會實例化 Singleton5,而是在第一次調用 getInstance() 加載內部類 SigletonHolder,此時才進行初始化 instance 成員變量,確保內存中的對象惟一性。
public class Singleton5 {
private Singleton5() {}
private static class SigletonHolder {
private final static Singleton5 instance = new Singleton5();
}
public static Singleton5 getInstance() {
return SigletonHolder.instance;
}
}複製代碼
假設,咱們如今有一個計數類 Counter 用來統計累加次數,每次調用 plus() 方法會進行累加。
public class Counter {
private long count = 0;
public long plus(){
return ++count;
}
}複製代碼
這個案例的實現方式會生成多個實例,那麼咱們如何使用單例模式確保只生成一個實例對象呢?
實際上,拆解成3個步驟就能夠實現個人需求:靜態類成員變量、私有的構造方法、全局訪問方法。
public class Counter {
private long count = 0;
private static Counter counter = new Counter();
private Counter(){}
public static Counter getInstance(){
return counter;
}
public synchronized long plus(){
return ++count;
}
}複製代碼
基於單例模式,咱們還能夠進行擴展改造,獲取指定個數的對象實例,節省系統資源,並解決單例對象共享過多有性能損耗的問題。
咱們來作個練習,我如今有一個需求,但願實現最多隻能生成 2 個 Resource 類的實例,能夠經過 getInstance() 方法進行訪問。
public class Resource {
private int id = 0;
private static Resource[] resource = new Resource[]{
new Resource(1),
new Resource(2)
};
private Resource(int id){
this.id = id;
}
public static Resource getInstance(int id){
return resource[id];
}
}複製代碼
若是認爲單例模式是非靜態方法。而靜態方法和非靜態方法,最大的區別在因而否常駐內存,其實是不對的。它們都是在第一次加載後就常駐內存,因此方法自己在內存裏,沒有什麼區別,因此也就不存在靜態方法常駐內存,非靜態方法只有使用的時候才分配內存的結論。
所以,咱們要從場景的層面來剖析這個問題。若是一個方法和他所在類的實例對象無關,僅僅提供全局訪問的方法,這種狀況考慮使用靜態類,例如 java.lang.Math。而使用單例模式更加符合面向對象思想,能夠經過繼承和多態擴展基類。此外,上面的案子中,單例模式還能夠進行延伸,對實例的建立有更自由的控制。
數據庫鏈接並非單例的,若是一個系統中只有一個數據庫鏈接實例,那麼所有數據訪問都使用這個鏈接實例,那麼這個設計確定致使性能缺陷。事實上,咱們經過單例模式確保數據庫鏈接池只有一個實例存在,經過這個惟一的鏈接池實例分配 connection 對象。
單例模式的目的在於,一個類只有一個實例存在,即保證一個類在內存中的對象惟一性。
若是採用餓漢式,在類被加載時就實例化,所以無須考慮多線程安全問題,而且對象一開始就得以建立,性能方面要優於懶漢式。
若是採用懶漢式,採用延遲加載,在第一次調用 getInstance() 方法時才實例化。好處在於無須一直佔用系統資源,在須要的時候再進行加載實例。可是,要特別注意多線程安全問題,咱們須要考慮使用雙重校驗鎖的方案進行優化。
實際上,咱們應該採用餓漢式仍是採用懶漢式,取決於咱們但願空間換取時間,仍是時間換取空間的抉擇問題。
此外,枚舉和靜態內部類也是很是不錯的實現方式。
(書)「圖解設計模式」(結城浩)
相關示例完整代碼: design-pattern-action
(完)
更多精彩文章,盡在「服務端思惟」微信公衆號!