什麼是單例
單例模式,也叫單子模式,是一種常用的軟件設計模式。在應用這個模式時,單例對象的類必須保證只有一個實例存在。
我們知道,在面向對象的思想中,通過類的構造函數可以創建對象,只要內存足夠,可以創建任意個對象。所以,要想限制某一個類只有一個單例對象,就需要在他的構造函數上下功夫。
實現對象單例模式的思路是:
1、一個類能返回對象一個引用(永遠是同一個)和一個獲得該實例的方法(必須是靜態方法,通常使用getInstance這個名稱);
2、當我們調用這個方法時,如果類持有的引用不爲空就返回這個引用,如果類保持的引用爲空就創建該類的實例並將實例的引用賦予該類保持的引用;
3、同時我們還將該類的構造函數定義爲私有方法,這樣其他處的代碼就無法通過調用該類的構造函數來實例化該類的對象,只有通過該類提供的靜態方法來得到該類的唯一實例。
最簡單的單例代碼:
/**
* @author 633805 LYH
* @version V1.0
* @description 單例的最簡單寫法 不加鎖 將該類的構造函數定義爲私有方法,這樣其他處的代碼就無法通過調用該類的構造函數來實例化該類的對象,只有通過該類提供的靜態方法來得到該類的唯一實例
* @create 2019-05-24 8:31
* @since 1.7
*/
public class Singleton3 {
private static Singleton3 singleton3;
private void Singleton3() {
}
public static Singleton3 getSingleton3() {
if (singleton3 == null) {
singleton3 = new Singleton3();
}
return singleton3;
}
}
以上Java代碼,就實現了一個簡單的單例模式。我們通過將構造方法定義爲私有,然後提供一個getInstance方法,該方法中來判斷是否已經存在該類的實例,如果存在直接返回。如果不存在則創建一個再返回。
線程安全的單例
如果有兩個線程同時執行到if(instance==null)這行代碼,這是判斷都會通過,然後各自會執行instance = new Singleton();並各自返回一個instance,這時候就產生了多個實例,就沒有保證單例!
上面這種單例的實現方式我們通常稱之爲懶漢模式,所謂懶漢,指的是隻有在需要對象的時候纔會生成(getInstance方法被調用的時候纔會生成)。上面的這種懶漢模式並不是線程安全的,所以並不建議在日常開發中使用。基於這種模式,我們可以實現一個線程安全的單例的,如下:
/**
* @author 633805 LYH
* @version V1.0
* @description 懶漢模式 鎖的粒度太大
* @create 2019-05-24 14:40
* @since 1.7
*/
public class Singleton4 {
private static Singleton4 singleton4;
private void Singleton4() {}
public static synchronized Singleton4 getInstance() {
if (singleton4 == null) {
singleton4 = new Singleton4();
}
return singleton4;
}
}
通過在getInstance方法上增加synchronized,通過鎖來解決併發問題。這種實現方式就不會發生有多個對象被創建的問題了。
雙重校驗鎖
上面這種線程安全的懶漢寫法能夠在多線程中很好的工作,但是,遺憾的是,這種做法效率很低,因爲只有第一次初始化的時候才需要進行併發控制,大多數情況下是不需要同步的。
我們其實可以把上述代碼做一些優化的,因爲懶漢模式中使用synchronized定義一個同步方法,我們知道,synchronized還可以用來定義同步代碼塊,而同步代碼塊的粒度要比同步方法小一些,從而效率就會高一些。如以下代碼:
/**
* @author 633805 LYH
* @version V1.0
* @description 雙重校驗鎖的實現方式,靜態成員變量singleton必須通過volatile來修飾,保證其初始化不被重排,否則可能被引用到一個未初始化完成的對象。
* @create 2019-05-24 14:46
* @since 1.7
*/
public class Singleton5 {
private volatile static Singleton5 singleton5;
private Singleton5 (){}
public static Singleton5 getSingleton() {
if (singleton5 == null) {
synchronized (Singleton5.class) {
if (singleton5 == null) {
singleton5 = new Singleton5();
}
}
}
return singleton5;
}
}
上面這種形式,只有在singleton == null的情況下再進行加鎖創建對象,如果singleton!=null的話,就直接返回就行了,並沒有進行併發控制。大大的提升了效率。
從上面的代碼中可以看到,其實整個過程中進行了兩次singleton == null的判斷,所以這種方法被稱之爲"雙重校驗鎖"。
還有值得注意的是,雙重校驗鎖的實現方式中,靜態成員變量singleton必須通過volatile來修飾,保證其初始化不被重排,否則可能被引用到一個未初始化完成的對象。
餓漢模式
前面提到的懶漢模式,其實是一種lazy-loading思想的實踐,這種實現有一個比較大的好處,就是隻有真正用到的時候才創建,如果沒被使用到,就一直不會被創建,這就避免了不必要的開銷。
但是這種做法,其實也有一個小缺點,就是第一次使用的時候,需要進行初始化操作,可能會有比較高的耗時。如果是已知某一個對象一定會使用到的話,其實可以採用一種餓漢的實現方式。
所謂餓漢,就是事先準備好,需要的時候直接給你就行了。這就是日常中比較常見的"先買票後上車",走正常的手續。
如以下代碼,餓漢模式:
/**
* @author 633805 LYH
* @version V1.0
* @description 餓漢模式
* @create 2019-05-24 14:49
* @since 1.7
*/
public class Singleton6 {
private static Singleton6 singleton6 = new Singleton6();
private Singleton6() {}
public static Singleton6 getInstance() {
return singleton6;
}
//第二種 餓漢變種
/* private Singleton6 instance = null;
static {
instance = new Singleton6();
}
private Singleton6 (){}
public static Singleton6 getInstance() {
return this.instance;
}*/
}
餓漢模式中的靜態變量是隨着類加載時被完成初始化的。餓漢變種中的靜態代碼塊也會隨着類的加載一塊執行。
以上兩個餓漢方法,其實都是通過定義靜態的成員變量,以保證instance可以在類初始化的時候被實例化。
因爲類的初始化是由ClassLoader完成的,這其實是利用了ClassLoader的線程安全機制。ClassLoader的loadClass方法在加載類的時候使用了synchronized關鍵字。也正是因爲這樣, 除非被重寫,這個方法默認在整個裝載過程中都是同步的(線程安全的)
除了以上兩種餓漢方式,還有一種實現方式也是藉助了calss的初始化來實現的,那就是通過靜態內部類來實現的單例:
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
前面提到的餓漢模式,只要Singleton類被裝載了,那麼instance就會被實例化。
而這種方式是Singleton類被裝載了,instance不一定被初始化。因爲SingletonHolder類沒有被主動使用,只有顯示通過調用getInstance方法時,纔會顯示裝載SingletonHolder類,從而實例化instance。
使用靜態內部類,藉助了classloader來實現了線程安全,這與餓漢模式有着異曲同工之妙,但是他有兼顧了懶漢模式的lazy-loading功能,相比較之下,有很大優勢。
單例的破壞
前文介紹過,我們實現的單例,把構造方法設置爲私有方法來避免外部調用是很重要的一個前提。但是,私有的構造方法外部真的就完全不能調用了麼?
其實不是的,我們是可以通過反射來調用類中的私有方法的,構造方法也不例外,所以,我們可以通過反射來破壞單例。
除了這種情況,還有一種比較容易被忽視的情況,那就是其實對象的序列化和反序列化也會破壞單例。
如使用ObjectInputStream進行反序列化時,在ObjectInputStream的readObject生成對象的過程中,其實會通過反射的方式調用無參構造方法新建一個對象。
所以,在對單例對象進行序列化以及反序列化的時候,一定要考慮到這種單例可能被破壞的情況。
可以通過在Singleton類中定義readResolve的方式,解決該問題:
/**
* 使用雙重校驗鎖方式實現單例
*/
public class Singleton implements Serializable{
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
private Object readResolve() {
return singleton;
}
}
枚舉實現單例
/**
* @author 633805 LYH
* @version V1.0
* @description 枚舉實現單例
* @create 2019-05-24 14:59
* @since 1.7
*/
public enum Singleton7 {
INSTANCE;
public void whateverMethod() {}
}
以上,就實現了一個非常簡單的單例,從代碼行數上看,他比之前介紹過的任何一種都要精簡,並且,他還是線程安全的。
這些,其實還不足以說服我們這種方式最優。但是還有個至關重要的原因,那就是:枚舉可解決反序列化會破壞單例的問題
關於枚舉實現單例可以參考:爲什麼我牆裂建議大家使用枚舉來實現單例
不使用synchronized實現單例
前面講過的所有方式,只要是線程安全的,其實都直接或者間接用到了synchronized,那麼,如果不能使用synchronized的話,怎麼實現單例呢?
使用Lock?這當然可以了,但是其實根本還是加鎖,有沒有不用鎖的方式呢?
答案是有的,那就是CAS。CAS是一項樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。
在JDK1.5 中新增java.util.concurrent(J.U.C)就是建立在CAS之上的。相對於對於synchronized這種阻塞算法,CAS是非阻塞算法的一種常見實現。所以J.U.C在性能上有了很大的提升。
藉助CAS(AtomicReference)實現單例模式:
import java.util.concurrent.atomic.AtomicReference;
/**
* @author 633805 CAS(AtomicReference)實現單例模式
* @version V1.0
* @description 對類的描述
* @create 2019-05-24 15:01
* @since 1.7
*/
public class Singleton8 {
private static final AtomicReference<Singleton8> INSTANCE = new AtomicReference<Singleton8>();
private Singleton8() {}
public static Singleton8 getInstance() {
for (;;) {
Singleton8 singleton = INSTANCE.get();
if (null != singleton) {
return singleton;
}
singleton = new Singleton8();
if (INSTANCE.compareAndSet(null, singleton)) {
return singleton;
}
}
}
}
用CAS的好處在於不需要使用傳統的鎖機制來保證線程安全,CAS是一種基於忙等待的算法,依賴底層硬件的實現,相對於鎖它沒有線程切換和阻塞的額外消耗,可以支持較大的並行度。不過CAS只能只能實現單個共享變量的原子性,而且失敗會不斷自旋,還容易出現ABA的問題。