版權聲明:本文為博主原創文章,轉載請注明出處,歡迎交流學習!
單例,顧名思義一個類只有一個實例。為什么要使用單例模式,或者說什么樣的類可以做成單例的?在工作中我發現,使用單例模式的類都有一個共同點,那就是這個類沒有狀態,也就是說無論你實例化多少個對象,其實都是一樣的。又或者是一個類需要頻繁實例化然后銷毀對象。還有很重要的一點,如果這個類有多個實例的話,會產生程序錯誤或者不符合業務邏輯。這種情況下,如果我們不把類做成單例,程序中就會存在多個一模一樣的實例,這樣會造成內存資源的浪費,而且容易產生程序錯誤。總結一下,判斷一個類是否要做成單例,最簡單的一點就是,如果這個類有多個實例會產生錯誤,或者在整個應用程序中,共享一份資源。
在實際開發中,一些資源管理器、數據庫連接等常常設計成單例模式,避免實例重復創建。實現單例有幾種常用的方式,下面我們來探討一下他們各自的優劣。
第一種方式:懶漢式單例
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 }
在不考慮並發的情況下,這是標准的單例構造方式,它通過以下幾個要點來保證我們獲得的實例是單一的。
1、靜態實例,靜態的屬性在內存中是唯一的;
2、私有的構造方法,這就保證了不能人為的去調用構造方法來生成一個實例;
3、提供公共的靜態方法來返回一個實例, 把這個方法設置為靜態的是有原因的,因為這樣我們可以通過類名來直接調用此方法(此時我們還沒有獲得實例,無法通過實例來調用方法),而非靜態的方法必須通過實例來調用,因此這里我們要把它聲明為靜態的方法通過類名來調用;
4、判斷只有持有的靜態實例為null時才通過構造方法產生一個實例,否則直接返回。
在多線程環境下,這種方式是不安全,通過自己的測試,多個線程同時訪問它可能生成不止一個實例,我們通過程序來驗證這個問題:
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類被加載就會產生一個靜態實例,而類被加載的原因有很多種,事實上我們可能從始至終都沒有使用這個實例,這樣會造成內存的浪費。在實際開發中,這個問題影響不大。
以上內容介紹了幾種常見的單例模式的實現方式,分析了在多線程情況下的處理方式, 在工作中可根據實際需要選擇合適的實現方式。還有一種利用枚舉來實現單例的方式,在工作中很少有人這樣寫過,不做探討。
