一、簡介
單例模式(Singleton Pattern),就是採取一定的方法保證在整個軟件系統內,一個類只有一個對象實例,並且該類只提供一個取得其對象實例的方法。
單例模式的類也可以和一般的類一樣,具有一般的數據和方法。一般的單例模式寫法分爲這三個步驟:
- 私有化構造方法
- 在類內部創建類的實例(私有的)
- 提供唯一一個獲取唯一實例的方法
二、不同寫法
1.線程不安全的懶漢式
懶漢式,即延遲實例化(lazy instantiaze),當我們需要時才進行創建,我們不需要某個類的實例時,它就永遠不會產生。具體代碼如下:
class Singleton {
// 聲明一個靜態變量來記錄Singleton的唯一實例
private static Singleton instance;
// 構造方法私有化,防止類的外部使用new創建該類的對象
private Singleton() {
}
// 提供公有的靜態方法,當對象爲null才實例化對象,並返回該實例
public static Singleton getInstance() {
// 如果instance不存在,則使用私用的構造器創建
if (instance == null) {
instance = new Singleton();
}
return instance;
}
//其他的方法
}
爲什麼說這種寫法是線程不安全的呢?這是因爲當有兩個線程同時進行Singleton類的創建時,就會出現如下圖的情況:
總結:這種情況下,一個線程進入了if(instance==null)語句塊,還沒來得及完成實例創建,另一個線程也進入到了這個語句塊,這樣就會產生兩個實例。所以在多線程環境下這種寫法是不可用的。
2.線程安全的懶漢式(同步方法)
要解決線程安全問題,只要把getInstance()方法改成同步(synchronized)方法就可以輕易解決。
class Singleton {
// 聲明一個靜態變量來記錄Singleton的唯一實例
private static Singleton instance;
// 構造方法私有化,防止類的外部使用new創建該類的對象
private Singleton() {
}
// 提供公有的靜態方法,當對象爲null才實例化對象,並返回該實例
public static synchronized Singleton getInstance() {
// 如果instance不存在,則使用私用的構造器創建
if (instance == null) {
instance = new Singleton();
}
return instance;
}
//其他的方法
}
這裏通過增加synchronized關鍵字到getInstance()方法中,迫使每個線程在進入這個方法前,需要等候別的線程離開該方法。(不會有多個線程同時進入該方法)
總結:這種寫法雖然解決了線程安全問題,但是這會降低性能,因爲每個線程要獲得該類的實例時,都需要在getInstance()方法進行同步,然而實際上,只有第一次執行此方法時才真正需要同步。
爲了符合大多數Java應用程序,我們需要確保單例模式在多線程環境下正常工作,但是上面的同步getInstance()方法的做法又會降低性能。
那麼下面的寫法都可以用於比較有效地解決多線程訪問問題。
3.餓漢式(靜態常量)
當Java應用程序總是需要創建並使用單例的實例,或者創建和運行方面的負擔不太繁重時,就可以使用餓漢式(在類裝載時就完成實例化)。
class Singleton {
// 1.構造方法私有化,防止類的外部使用new創建該類的對象
private Singleton() {}
// 2.類內部創建對象,該實例對象作爲靜態常量
private final static Singleton INSTANCE = new Singleton();
// 3.提供公有的靜態方法,返回實例對象
public static Singleton getInstance() {
return INSTANCE;
}
}
總結:利用這個寫法,優點在於我們依賴JVM在加載這個類時就創建了唯一的實例對象,這就避免了線程同步問題。而缺點在於若在程序創建到銷燬的過程都沒用到該類,就造成了內存浪費。
4.餓漢式(靜態代碼塊)
這種寫法將類的實例化過程放在靜態代碼塊中,同樣是在類被JVM加載時就完成實例化。同樣是餓漢式,優缺點與上一個寫法相同。
class Singleton {
// 1.構造方法私有化,防止類的外部使用new創建該類的對象
private Singleton() {}
// 2.此處聲明變量,該實例對象作爲靜態變量
private static Singleton instance;
static {//在靜態代碼塊中完成實例化
instance = new Singleton();
}
// 3.提供公有的靜態方法,返回實例對象
public static Singleton getInstance() {
return instance;
}
}
5.雙重檢查鎖(DCL)
利用雙重檢查加鎖(double checked locking) ,將鎖的範圍從方法移到方法內部。首先檢查實例是否已經創建,當沒有創建才進行線程同步 。這樣一來,就只會在第一次創建實例時進行同步。
class Singleton {
// 1.構造方法私有化,防止類的外部使用new創建該類的對象
private Singleton() {}
// 2.聲明Singleton類型的靜態變量
private static volatile Singleton instance;
// 3.提供公有的靜態方法,當對象爲null才實例化該類(加入synchronized關鍵字)
public static Singleton getInstance() {
if (instance == null) {// 如果對象爲null,才進入同步區塊
synchronized (Singleton.class) {
if (instance == null) {// 再檢查一次,對象仍爲null才初始化爲Singleton實例。
instance = new Singleton();
}
}
}
return instance;
}
}
這裏複習一下volatile:
首先需要明確的是,volatile不能說是輕量級的synchronized,因爲volatile不保證原子性(線程安全),只保證可見性,volatile只是輕量級的線程可見方式。
volatile關鍵字的兩大特性:
- 保證內存(線程)的可見性。當被volatile修飾的變量被修改時,JMM(Java內存模型)會把該線程本地內存(本地內存保存了主內存中的共享變量副本)中的變量強制刷新到主內存中去,並使其他線程中的緩存無效,需要重新在主內存中讀取。(保證了變量被修改後,其他線程立刻知道)
- 禁止指令重排。重排序是指編譯器和處理器爲了優化程序性能而對指令序列進行排序的一種手段。
以上面的單例模式的雙重檢查爲例,這條語句:instance = new Singleton();
的執行,可以分爲3條僞代碼:
(a) memory = allocate() //分配內存
(b) ctorInstanc(memory) //初始化對象
(c) instance = memory //設置instance指向剛分配的地址
如果沒有volatile關鍵字修飾該變量,上面的代碼在編譯運行時,可能會出現重排序:從a-b-c
排序爲a-c-b
。那麼在多線程的情況下會出現以下問題:當線程A在執行第13行代碼:instance=new Singleton()
時,B線程進來執行到第10行代碼:(if (install==null)
)。假設此時A執行的過程中發生了指令重排序,即先執行了a和c,沒有執行b。那麼由於A線程執行了c導致instance指向了一段地址,所以B線程判斷instance不爲null,會直接跳到第6行並返回一個未初始化的對象。
所以這裏聲明變量使用到的volatile關鍵字確保了當instance變量被初始化爲Singleton實例時,多個線程正確地處理instance變量。
總結:這種寫法既實現了懶漢式的延遲加載和線程安全的保證,又大大減少了getInstance()的時間消耗。在實際開發中,推薦使用這種單例模式寫法。
volatile關鍵字學習:Java中volatile關鍵字的最全總結
6.靜態內部類
靜態內部類的寫法採用了類裝載的機制來保證初始化實例時只有一個線程。
class Singleton {
// 1.構造方法私有化,防止類的外部使用new創建該類的對象
private Singleton() {}
// 2.寫一個靜態內部類,其中有一個靜態屬性Singleton
private static class SingletonInstance {
private static final Singleton instance = new Singleton();
}
// 3.提供公有的靜態方法獲取內部類中的屬性
public static Singleton getInstance() {
return SingletonInstance.instance;
}
}
採用靜態內部類方式,使得instance在Singleton類被裝載時不會立即初始化,而是在需要使用時,調用getInstance()方法後纔會裝載靜態內部類SingleInstance,這才完成Singleton類的對象instance的初始化。
總結:使用靜態內部類方式既避免了線程不安全,又利用靜態內部類特點(產生了對該類的引用,纔會裝載到內存中)實現延遲加載,所以在開發中推薦使用。
7.枚舉
藉助JDK1.5中添加的枚舉來實現單例模式。不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象。
enum Singleton {
INSTANCE;
}
這種方式是《Effective Java》的作者Josh Bloch提倡的方式。
三、具體應用
在JDK中的java.lang.Runtime就是經典的單例模式,採用的是餓漢式,類一裝載就創建該類的實例。
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
//省略
}
四、總結
- 作用:單例模式保證了系統內存中一個類只存在一個對象,節省了系統資源,對於一些需要頻繁創建和銷燬的對象,使用單例模式可以提高系統性能。
- 使用場景:需要頻繁的進行創建和銷燬的對象、創建對象耗時過長或耗費資源過多的對象但又經常使用的對象。如:工具類對象、頻繁訪問數據庫或文件的對象。
- 注意:每個類加載器都定義了一個命名空間,如果有兩個以上的類加載器,不同的類加載器可能會加載同一個類。這種情況下,使用單例模式需要自動指定同一個類加載器。