學習java的同學注意了!!! 學習過程中遇到什么問題或者想獲取學習資源的話,歡迎加入Java學習交流群,群號碼:523047986 我們一起學Java!
在GoF的23種設計模式中,單例模式是比較簡單的一種。然而,有時候越是簡單的東西越容易出現問題。下面就單例設計模式詳細的探討一下。
所謂單例模式,簡單來說,就是在整個應用中保證只有一個類的實例存在。就像是Java Web中的application,也就是提供了一個全局變量,用處相當廣泛,比如保存全局數據,實現全局性的操作等。
1、最簡單的實現
首先,能想到的最簡單的實現是,把類的構造函數寫成PRivate的,從而保證別的類不能實例化此類。然后在類中返回一個靜態示例并返回給調用者。這樣,調用者就可以通過這個引用使用這個實例了。
public class Singleton{ private static final Singleton singleton = new Singleton(); public static Singleton getInstance(){ return singleton; } private Singleton(){ }}如上例,外部使用者如果需要使用SingletonClass的實例,只能通過getInstance()方法,并且它的構造方法是private的,這樣就保證了只能有一個對象存在。
2、性能優化--lazy loaded
上面的代碼雖然簡單,但是有一個問題----無論這個類是否被使用,都會創建一個instance對象。如果這個創建很耗時,比如說鏈接10000次數據庫(夸張一點啦....),并且這個類還不一定會被使用,那么這個創建過程就是無用的,怎么辦呢?
為了解決這個問題,我們想到的新的解決方案:
public class SingletonClass { private static SingletonClass instance = null; public static SingletonClass getInstance() { if(instance == null) { instance = new SingletonClass(); } return instance; } private SingletonClass() { } }代碼的變化有倆處----首先,把 instance 設置為 null ,知道第一次使用的時候判是否為 null 來創建對象。因為創建對象不在聲明處,所以那個 final 的修飾必須去掉。
我們來想象一下這個過程。要使用 SingletonClass ,調用 getInstance()方法,第一次的時候發現instance時null,然后就創建一個對象,返回出去;第二次再使用的時候,因為這個instance事static的,共享一個對象變量的,所以instance的值已經不是null了,因此不會再創建對象,直接將其返回。
這個過程就稱為lazy loaded ,也就是遲加載-----直到使用的時候才經行加載。
3、同步
上面的代碼很清楚,也很簡單。然而就像那句名言:“80%的錯誤是由20%的代碼優化引起的”。單線程下,這段代碼沒什么問題,可是如果是多線程呢,麻煩就來了,我們來分析一下:
線程A希望使用SingletonClass,調用getInstance()方法。因為是第一次調用,A就發現instance是null的,于是它開始創建實例,就在這個時候,CPU發生時間片切換,線程B開始執行,它要使用SingletonClass,調用getInstance()方法,同樣檢測到instance是null——注意,這是在A檢測完之后切換的,也就是說A并沒有來得及創建對象——因此B開始創建。B創建完成后,切換到A繼續執行,因為它已經檢測完了,所以A不會再檢測一遍,它會直接創建對象。這樣,線程A和B各自擁有一個SingletonClass的對象——單例失敗!
解決的辦法也很簡單,那就是加鎖:
public class SingletonClass{ private static SingletonClass instance = null; public synchronized static SingletonClass getInstance(){ if(instance == null){ instance = new SingletonClass(); } return instance; } private SingletonClass(){ } }只要getInstance()加上同步鎖,,一個線程必須等待另外一個線程創建完后才能使用這個方法,這就保證了單利的唯一性。
4、又是性能
上面的代碼又是很清楚也很簡單的,然而,往往簡單的東西不夠理想。這段代碼毫無疑問存在性能的問題----synchronized修飾的同步塊可是要比一般的代碼慢上幾倍的!如果存在很多次的getInstance()調用,那性能問題就不得不考慮了?!!!
讓我們來分析一下,究竟是整個方法都必須加鎖,還是緊緊其中某一句加鎖就足夠了?我們為什么要加鎖呢?分析一下lazy loaded的那種情形的原因,原因就是檢測null的操作和創建對象的操作分離了,導致出現只有加同步鎖才能單利的唯一性。
如果這倆個操作能夠原子的進行,那么單利就已經保證了。于是,我們開始修改代碼:
public class SingletonClass{ private static SingletonClass instance = null; public static SingletonClass getInstance(){ synchronized(SingletonClass.class){ if(instance == null){ instance = new SingletonClass(); } } return instance; } private SingletonClass(){ } }首先去掉 getInstance() 的操作,然后把同步鎖加載到if語句上。但是,這樣的修改起不到任何作用:因為每次調用getInstance()的時候必然要經行同步,性能的問題還是存在。如果............我們事先判斷一下是不是為null在去同步呢?
public class SingletonClass{ private static SingletonClass instance = null; public static SingletonClass getInstance(){ if(instance == null){ synchronized(SingletonClass.class){ if(instance == null){ instance = new SingletonClass(); } } } return instance; } private SingletonClass(){ } }還有問題嗎?首先判斷instance是不是為null,如果為null在去進行同步,如果不為null,則直接返回instance對象。
這就是double---checked----locking 設計實現單利模式。到此為止,一切都很完美。我們用一種很聰明的方式實現了單例模式。
5、從源頭檢查
下面我們開始說編譯原理。所謂編譯,就是把源代碼”翻譯“成目標代碼----大多是是指機器代碼----的過程。針對Java,它的目標代碼不是本地機器代碼,而是虛擬機代碼。編譯原理里面有一個很重要的內容是編譯器優化。所謂編譯器優化是指,在不改變原來語義的情況下,通過調整語句順序,來讓程序運行的更快。這個過程成為reorder。
要知道,JVM只是一個標準,并不是實現。JVM中并沒有規定有關編譯器優化的內容,也就是說,JVM實現可以自由的進行編譯器優化。
下面來想一下,創建一個變量需要哪些步驟呢?一個是申請一塊內存,調用構造方法進行初始化操作,另一個是分配一個指針指向這塊內存。這兩個操作誰在前誰在后呢?JVM規范并沒有規定。那么就存在這么一種情況,JVM是先開辟出一塊內存,然后把指針指向這塊內存,最后調用構造方法進行初始化。
下面我們來考慮這么一種情況:線程A開始創建SingletonClass的實例,此時線程B調用了getInstance()方法,首先判斷instance是否為null。按照我們上面所說的內存模型,A已經把instance指向了那塊內存,只是還沒有調用構造方法,因此B檢測到instance不為null,于是直接把instance返回了——問題出現了,盡管instance不為null,但它并沒有構造完成,就像一套房子已經給了你鑰匙,但你并不能住進去,因為里面還沒有收拾。此時,如果B在A將instance構造完成之前就是用了這個實例,程序就會出現錯誤了!
于是,我們想到了下面的代碼:
public class SingletonClass { private static SingletonClass instance = null; public static SingletonClass getInstance() { if (instance == null) { SingletonClass sc; synchronized (SingletonClass.class) { sc = instance; if (sc == null) { synchronized (SingletonClass.class) { if(sc == null) { sc = new SingletonClass(); } } instance = sc; } } } return instance; } private SingletonClass() { } }我們在第一個同步塊里面創建一個臨時變量,然后使用這個臨時變量進行對象的創建,并且在最后把instance指針臨時變量的內存空間。寫出這種代碼基于以下思想,即synchronized會起到一個代碼屏蔽的作用,同步塊里面的代碼和外部的代碼沒有聯系。因此,在外部的同步塊里面對臨時變量sc進行操作并不影響instance,所以外部類在instance=sc;之前檢測instance的時候,結果instance依然是null。 不過,這種想法完全是錯誤的!同步塊的釋放保證在此之前——也就是同步塊里面——的操作必須完成,但是并不保證同步塊之后的操作不能因編譯器優化而調換到同步塊結束之前進行。因此,編譯器完全可以把instance=sc;這句移到內部同步塊里面執行。這樣,程序又是錯誤的了!6. 解決方案了這么多,難道單例沒有辦法在Java中實現嗎?其實不然! 在JDK 5之后,Java使用了新的內存模型。volatile關鍵字有了明確的語義——在JDK1.5之前,volatile是個關鍵字,但是并沒有明確的規定其用途——被volatile修飾的寫變量不能和之前的讀寫代碼調整,讀變量不能和之后的讀寫代碼調整!因此,只要我們簡單的把instance加上volatile關鍵字就可以了。
public class SingletonClass { private volatile static SingletonClass instance = null; public static SingletonClass getInstance() { if (instance == null) { synchronized (SingletonClass.class) { if(instance == null) { instance = new SingletonClass(); } } } return instance; } private SingletonClass() { } }然而,這只是JDK1.5之后的Java的解決方案,那之前版本呢?其實,還有另外的一種解決方案,并不會受到Java版本的影響:
public class SingletonClass { private static class SingletonClassInstance { private static final SingletonClass instance = new SingletonClass(); } public static SingletonClass getInstance() { return SingletonClassInstance.instance; } private SingletonClass() { } }在這一版本的單例模式實現代碼中,我們使用了Java的靜態內部類。這一技術是被JVM明確說明了的,因此不存在任何二義性。在這段代碼中,因為SingletonClass沒有static的屬性,因此并不會被初始化。直到調用getInstance()的時候,會首先加載SingletonClassInstance類,這個類有一個static的SingletonClass實例,因此需要調用SingletonClass的構造方法,然后getInstance()將把這個內部類的instance返回給使用者。由于這個instance是static的,因此并不會構造多次。 由于SingletonClassInstance是私有靜態內部類,所以不會被其他類知道,同樣,static語義也要求不會有多個實例存在。并且,JSL規范定義,類的構造必須是原子性的,非并發的,因此不需要加同步塊。同樣,由于這個構造是并發的,所以getInstance()也并不需要加同步。
至此,我們完整的了解了單例模式在Java語言中的時候,提出了兩種解決方案。個人偏向于第二種,并且Effiective Java也推薦的這種方式。
學習Java的同學注意了!!! 學習過程中遇到什么問題或者想獲取學習資源的話,歡迎加入Java學習交流群,群號碼:523047986 我們一起學Java!
新聞熱點
疑難解答