版權聲明:本文爲博主原創文章,轉載請註明出處,歡迎交流學習!數據庫
單例,顧名思義一個類只有一個實例。爲何要使用單例模式,或者說什麼樣的類能夠作成單例的?在工做中我發現,使用單例模式的類都有一個共同點,那就是這個類沒有狀態,也就是說不管你實例化多少個對象,其實都是同樣的。又或者是一個類須要頻繁實例化而後銷燬對象。還有很重要的一點,若是這個類有多個實例的話,會產生程序錯誤或者不符合業務邏輯。這種狀況下,若是咱們不把類作成單例,程序中就會存在多個如出一轍的實例,這樣會形成內存資源的浪費,並且容易產生程序錯誤。總結一下,判斷一個類是否要作成單例,最簡單的一點就是,若是這個類有多個實例會產生錯誤,或者在整個應用程序中,共享一份資源。安全
在實際開發中,一些資源管理器、數據庫鏈接等經常設計成單例模式,避免實例重複建立。實現單例有幾種經常使用的方式,下面咱們來探討一下他們各自的優劣。多線程
第一種方式:懶漢式單例併發
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類被加載就會產生一個靜態實例,而類被加載的緣由有不少種,事實上咱們可能從始至終都沒有使用這個實例,這樣會形成內存的浪費。在實際開發中,這個問題影響不大。
以上內容介紹了幾種常見的單例模式的實現方式,分析了在多線程狀況下的處理方式, 在工做中可根據實際須要選擇合適的實現方式。還有一種利用枚舉來實現單例的方式,在工做中不多有人這樣寫過,不作探討。