單例模式的幾種形式
1.餓漢式:優點是線程安全,在加載類的時候就建立一次并且隻會建立一次,缺點是還沒有使用就加載好了,是典型的空間換時間,當類裝載的時候就會建立類執行個體,即使你很長時間用不到它,不符合java的設計原則
public class HungrySingleMode {
private HungrySingleMode(){}
private static HungrySingleMode instance=new HungrySingleMode();
public static HungrySingleMode getInstance(){
return instance;
}
}
2.懶漢式,在使用的時候去建立執行個體,是典型的時間換空間,也就是每次擷取執行個體都會進行判斷,看是否需要建立執行個體,浪費判斷的時間。當然,如果一直沒有人使用的話,那就不會建立執行個體,則節約記憶體空間,懶漢式有兩種寫法,關鍵在于加鎖的位置
第一種寫法,把鎖加在建立執行個體的方法上,可以保證線程安全,但是效率較低,因為每次擷取執行個體都要進行鎖的操作,而實際上,隻有第一次建立執行個體的時候需要加鎖,在此調用因為執行個體已經存在,是以沒必要再鎖,這種方式降低了效率
public class LazySingleMode {
private LazySingleMode(){}
private static LazySingleMode instance;
public static synchronized LazySingleMode getInstance(){
if(instance == null){
instance = new LazySingleMode();
}
return instance;
}
}
第二種寫法,把鎖加載方法中,先進行執行個體的判斷,如果為null,加鎖建立,保證線程安全,如果不為null,直接傳回執行個體,這樣以來也就隻有第一次擷取執行個體的時候會進行鎖的操作,以後就不需要了,提高了效率
public class LazySingleMode {
private LazySingleMode(){}
private static LazySingleMode instance;
public static LazySingleMode getInstance(){
if(instance == null){
synchronized (LazySingleMode.class){
if(instance == null){
instance = new LazySingleMode();
}
}
}
return instance;
}
}
這樣看來加了鎖的懶漢式單例已經是絕對的線程安全了,其實不然
在高并發的多線程程式設計中,即便是如上我們已經加了鎖,其實也不是絕對安全的,JDK1.5之後處理器針對亂序指令進行指令重排仍會導緻這個問題,指令重排是JVM為了優化指令,提高程式運作效率,在不影響單線程程式執行結果的前提下,盡可能地提高并行度。編譯器、處理器也遵循這樣一個目标。注意是單線程。多線程的情況下指令重排序就會給程式員帶來問題。
拿上邊的懶漢式第二種寫法為例,在進行instance是否為null的判斷中
if(instance == null){
synchronized (LazySingleMode.class){
if(instance == null){
instance = new LazySingleMode();
}
}
}
進行指令重排之後的代碼為:
if(instance == null){
synchronized (LazySingleMode.class){
instance = new LazySingleMode();
}
}
為什麼第二個判斷沒有了呢,因為編譯器判斷這個程式在執行過程中,這個值是不會改變的,編譯器不考慮多線程的情況,這就是指令重排的結果,多線程程式設計的情況下指令重排序就會給程式員帶來問題。少了這個判斷,就會導緻,假設兩個線程同時走到了if(instance == null)的下邊,這時就會導緻執行個體被建立兩次
那麼如何避免,接下來就是一種線程絕對安全的單例模式的寫法,叫單例的DCL方式
public class LazySingleMode {
private LazySingleMode(){}
private static volatile LazySingleMode instance;
public static LazySingleMode getInstance(){
if(instance == null){
synchronized (LazySingleMode.class){
if(instance == null){
instance = new LazySingleMode();
}
}
}
return instance;
}
}
沒有增加什麼東西,僅僅是給LazySingleMode的執行個體添加了一個關鍵字:volatile
加了volatile,是告訴編譯器,這個變量随時有可能會被其他線程改變,這樣編譯器就不會把這兩個判斷優化成一個判斷了。也就是告訴編譯器,不要自作聰明的對我進行指令重排序
網上對volatile的解釋很詳細:
volatile的變量,可以保證對該變量的操作具有原子性,典型的例子是long和
double型變量,通常需要分兩步讀寫一個double變量,volatile修飾的double可以
保證對一個double變量的操作的兩部分不會被多線程插入。以及對引用類型指派
的,new一個執行個體的過程不會被其他線程插入(new在編譯指令中是分成幾步執
行的,防止這幾步在執行過程中被其他線程取這個變量值,取到一個不完整的實
例)。可以簡單的了解為對volatile變量的set和get方法加上了synchronized關鍵
字,在new的過程中,整個都處在set中,是以不會被其他線程的get打斷,取到
不完整的引用。
原子性針對一個long或者double或者一個引用類型,對于引用類型,原子性
是指在new執行個體的過程中,不會被其他線程取到,即不會被其他線程取到一個不
完整的執行個體。這種原子性可以了解為new的過程處于一個synchronize段的set方
法中,隻有set結束才可以被get到,即new的整個過程都是處于set中的。也可以
了解為指令重排序,禁止把new過程的指令與把引用指派給變量的語句重排序,
指派隻發生在new結束之後。
3.餓漢式變形版,我們知道餓漢式的單例模式是可以保證線程安全之一點的,但是唯一不好的地方就是在還沒有調用的時候就初始化了執行個體,這樣以來導緻還沒有使用就占用過多的空間,那麼有沒有一種辦法可以解決這個問題,讓餓漢式也可以實作使用的時候建立執行個體,下面我們就來實作這一點
public class InnerHolderSingleMode {
private InnerHolderSingleMode(){}
private static class ClassHolder{
private static InnerHolderSingleMode instance = new InnerHolderSingleMode();
}
public static InnerHolderSingleMode getInstance (){
return ClassHolder.instance;
}
}
大緻相同的寫法,差別就在于我們的類的執行個體是在這個類中内部類中建立的,這樣就可以實作當我們調用
getInstance方法的時候再去初始化外部類的執行個體,達到節省空間的目的,同樣也保證了線程安全
4.枚舉實作單例模式,有言論說枚舉是jdk1.5之後實作單例最好的方式
單元素的枚舉類型已經成為實作Singleton的最佳方法。
---------《Effective Java》
枚舉單例是如何被保證的:
首先,在枚舉中構造方法為私有,在我們通路枚舉執行個體時會執行構造方法,同時每個枚舉執行個體都是static final類型的,也就表明隻能被執行個體化一次。在調用構造方法時,我們的單例被執行個體化。
也就是說,因為enum中的執行個體被保證隻會被執行個體化一次,是以我們的INSTANCE也被保證執行個體化一次
public enum EnumSingleMode {
//EnumSingleMode可以看作一個類,INSTANCE就是這個類的執行個體,
//每個枚舉執行個體都是static final類型的,也就是說,編譯器編譯之後
//INSTANCE前是public static final,是以每個執行個體隻能被初始化一次
//那麼我們要調用這個枚舉中的方法或者執行個體,
//隻需要EnumSingleMode.INSTANCE.getInstance()
//或者EnumSingleMode.INSTANCE.instance
//當我們通路這個枚舉執行個體時會執行構造方法,此時我們的單例被執行個體化instance = new Teacher();
//因為enum中的執行個體被保證隻會被執行個體化一次(INSTANCE隻會執行個體化一次)
//是以構造方法也隻會執行一次,是以可以保證我們的類Teacher也被執行個體話一次
INSTANCE;
private Teacher instance;
//枚舉中的構造方法本身就是私有的,即便我們不加private,它也是私有
EnumSingleMode(){
instance = new Teacher();
System.out.println("EnumSingleMode構造");
}
public Teacher getInstance() {
return instance;
}
}