小夥們好,我是jack xu,今天跟你們講一個老生常談的話題,單例模式是最經常使用到的設計模式之一,熟悉設計模式的朋友對單例模式都不會陌生。網上的文章也不少,可是良莠不齊,參差不齊,要麼說的不到點子上,要麼寫的不完整,我試圖寫一篇史上最全單例模式,讓你看一篇文章就夠了。。java
單例模式是指確保一個類在任何狀況下都絕對只有一個實例,並提供一個全局訪問點。單例模式是建立型模式。許多時候整個系統只須要擁有一個全局對象,這樣有利於咱們協調系統總體的行爲。好比在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,而後服務進程中的其餘對象再經過這個單例對象獲取這些配置信息。這種方式簡化了在複雜環境下的配置管理。咱們寫單例的思路是,隱藏其全部構造方法,提供一個全局訪問點。面試
這個很簡單,小夥們都寫過,這個在類加載的時候就當即初始化,由於他很餓嘛,一開始就給你建立一個對象,這個是絕對線程安全的,在線程還沒出現之前就實例化了,不可能存在訪問安全問題。他的缺點是若是不用,用不着,我都佔着空間,形成內存浪費。算法
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
複製代碼
還有一種是餓漢式的變種,靜態代碼塊寫法,原理也是同樣,只要是靜態的,在類加載的時候就已經成功初始化了,這個和上面的比起來沒什麼區別,無非就是裝個b,看起來比上面那種吊,由於見過的人很少嘛。spring
public class HungryStaticSingleton {
private static final HungryStaticSingleton hungrySingleton;
static {
hungrySingleton = new HungryStaticSingleton();
}
private HungryStaticSingleton() {
}
public static HungryStaticSingleton getInstance() {
return hungrySingleton;
}
}
複製代碼
爲了解決餓漢式佔着茅坑不拉屎的問題,就產生了下面這種簡單懶漢式的寫法,一開始我先申明個對象,可是先不建立他,當用到的時候判斷一下是否爲空,若是爲空我就建立一個對象返回,若是不爲空則直接返回,爲何叫懶漢式,就是由於他很懶啊,要等用到的時候纔去建立,看上去很ok,可是在多線程的狀況下會產生線程安全問題。設計模式
public class LazySimpleSingleton {
private static LazySimpleSingleton instance;
private LazySimpleSingleton() {
}
public static LazySimpleSingleton getInstance() {
if (instance == null) {
instance = new LazySimpleSingleton();
}
return instance;
}
}
複製代碼
若是有兩個線程同時執行到 if (instance==null) 這行代碼,這是判斷都會經過,而後各自會執行instance = new Singleton(),並各自返回一個instance,這時候就產生了多個實例,就沒有保證單例,以下圖所示。。 安全
怎麼解決這個問題呢,很簡單,加鎖啊,加一下synchronized便可,這樣就能保住線程安全問題了上面這樣寫法帶來一個缺點,就是性能低,只有在第一次進行初始化的時候才須要進行併發控制,然後面進來的請求不須要在控制了,如今synchronized加在方法上,我管你生成沒成生成,只要來了就得給我排隊,因此這種性能是極其低下的,那怎麼辦呢?咱們知道,其實synchronized除了加在方法上,還能夠加在代碼塊上,只要對生成對象的那一部分代碼加鎖就能夠了,由此產生一種新的寫法,叫作雙重檢驗鎖,咱們看下面代碼。。(ps:對於synchronized想要更深一步瞭解的同窗,能夠看我另外一篇文章)bash
咱們看19行將synchronized包在了代碼塊上,當 singleton == null 的時候,咱們只對建立對象這一塊邏輯進行了加鎖控制,若是 singleton != null 的話,就直接返回,大大提高了效率。在21行的時候又加了一個singleton == null,這又是爲何呢,緣由是若是兩個線程都到了18行,發現是空的,而後都進入到代碼塊,這裏雖然加了synchronized,但做用只是進行one by one串行化,第一個線程往下走建立了對象,第二個線程等待第一個線程執行完畢後,我也往下走,因而乎又建立了一個對象,那仍是沒控制住單例,因此在21行當第二個線程往下走的時候在判斷一次,是否是被別的線程已經建立過了,這個就是雙重校驗鎖,進行了兩次非空判斷。咱們看到在11行的時候加了 volatile 關鍵字,這是用來防止指令重排的,當咱們建立對象的時候會通過下面幾個步驟,可是這幾個步驟不是原子的,計算機比較聰明,有時候爲了提升效率他不是按順序1234執行的,多是3214執行。這時候若是第一個線程執行了instance = new LazyDoubleCheckSingleton(),因爲指令重排先進行了第三步,先分配了一個內存地址,第二個線程進來的時候發現對象已是非null,直接返回,但這時候對象還沒初始化好啊,第二個線程拿到的是一個沒有初始化好的對象!這個就是要加volatile的緣由。服務器
1.分配內存給這個對象
2.初始化對象
3.設置instance指向剛分配的內存地址
4.初次訪問對象
複製代碼
最後說下雙重校驗鎖,雖然提升了性能,可是在我看來不夠優雅,折騰來折騰去,一會防這一會防那,尤爲是對新手不友好,新手會不明白爲何要這麼寫。。多線程
上面已經將鎖的粒度縮小到建立對象的時候了,但無論加在方法上仍是加在代碼塊上,終究仍是用到了鎖,只要用到鎖就會產生性能問題,那有沒有不用鎖的方式呢,答案是有的,那就是靜態內部類的方式,他實際上是利用了java代碼的一種特性,靜態內部類在主類加載的時候是不會被加載的,只有當調用getInstance()方法的時候纔會被加載進來進行初始化,代碼以下併發
/** * @author jack xu * 兼顧餓漢式的內存浪費,也兼顧synchronized性能問題 */
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton() {
}
public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton();
}
}
複製代碼
好,講到這裏我已經介紹了五種單例的寫法,通過層層的演進推理,到第五種的時候已是很完美的寫法了,既兼顧餓漢式的內存浪費,也兼顧synchronized性能問題,那他真的必定完美嗎,其實否則,他還有一個安全的問題,接下來咱們講下單例的破壞,有兩種方式反射和序列化。
咱們知道在上面單例的寫法中,在構造方法上加上private關鍵字修飾,就是爲了避免讓外部經過new的方式來建立對象,但還有一種暴力的方法,我就是不走尋常路,你不讓我new是吧,我反射給你建立出來,代碼以下
/** * @author jack xu */
public class ReflectDestroyTest {
public static void main(String[] args) {
try {
Class<?> clazz = LazyInnerClassSingleton.class;
Constructor c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
Object o1 = c.newInstance();
Object o2 = c.newInstance();
System.out.println(o1 == o2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製代碼
上面c.setAccessible(true)就是強吻,你private了,我如今把你的權限設爲true,我照樣可以訪問,經過c.newInstance()調用了兩次構造方法,至關於new了兩次,咱們知道 == 比的是地址,最後結果是false,確實是建立了兩個對象,反射破壞單例成功。
那麼如何防止反射呢,很簡單,就是在構造方法中加一個判斷public class LazyInnerClassSingleton {
private LazyInnerClassSingleton() {
if (LazyHolder.INSTANCE != null) {
throw new RuntimeException("不要試圖用反射破壞單例模式");
}
}
public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton();
}
}
複製代碼
在看結果,防止反射成功,當調用構造方法時,發現單例實例對象已經不爲空了,拋出異常,不讓你在繼續建立了。。
接下來介紹單例的另外一種破壞方式,先在靜態內部類上實現Serializable接口,而後寫個測試方法測試下,先建立一個對象,而後把這個對象先序列化,而後在反序列化出來,而後對比一下
public static void main(String[] args) {
LazyInnerClassSingleton s1 = null;
LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (LazyInnerClassSingleton) ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
複製代碼
咱們來看結果發現是false,在進行反序列化時,在ObjectInputStream的readObject生成對象的過程當中,其實會經過反射的方式調用無參構造方法新建一個對象,因此反序列化後的對象和手動建立的對象是不一致的。
那麼怎麼避免呢,依然很簡單,在靜態內部類里加一個readResolve方法便可private Object readResolve() {
return LazyHolder.INSTANCE;
}
複製代碼
在看結果就變成true了,爲何加了一個方法就能夠避免被序列化破壞呢,這裏不在展開,感興趣的小夥伴能夠看下ObjectInputStream的readObject()方法,一步步往下走,會發現最終會調用readResolve()方法。
至此,史上最牛b單例產生,已經無懈可擊、無可挑剔了。那麼這裏我爲何還要在介紹枚舉呢,在《Effective Java》中,枚舉是被推薦的一種方式,由於他足夠簡單,線程安全,也不會被反射和序列化破壞,你們看下才寥寥幾句話,不像上面雖然已經實現了最牛b的寫法,可是其中的過程很讓人煩惱啊,要考慮性能、內存、線程安全、破壞啊,一會這裏加代碼一會那裏加代碼,才能達到最終的效果。而使用枚舉,感興趣的小夥伴能夠反編譯看下,枚舉的底層其實仍是一個class類,而咱們考慮的這些問題JDK源碼其實幫咱們都已經實現好了,因此在 java 層面咱們只須要用三句話就能搞定!
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
複製代碼
至此,我經過層層演進,由淺入深的給你們介紹了單例的這麼多寫法,從不完美到完美,這麼多也是網上很常見的寫法,下面我在送你們兩個彩蛋,擴展一下其餘寫單例的方式方法。。
容器式單例是咱們 spring 中管理單例的模式,咱們平時在項目中會建立不少的Bean,當項目啓動的時候spring會給咱們管理,幫咱們加載到容器中,他的思路方式方法以下。。
public class ContainerSingleton {
private ContainerSingleton() {
}
private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getInstance(String className) {
Object instance = null;
if (!ioc.containsKey(className)) {
try {
instance = Class.forName(className).newInstance();
ioc.put(className, instance);
} catch (Exception e) {
e.printStackTrace();
}
return instance;
} else {
return ioc.get(className);
}
}
}
複製代碼
這個能夠說是一個簡易版的 spring 管理容器,你們看下這裏用一個map來保存對象,當對象存在的時候直接從map裏取出來返回出去,若是不存在先用反射建立一個對象出來,先保存到map中而後在返回出去。咱們來測試一下,先建立一個Pojo對象,而後兩次從容器中去取出來,比較一下,發現結果是true,證實兩次取出的對象是同一個對象。
可是這裏有一個問題,這樣的寫法是線程不安全的,那麼如何作到線程安全呢,這個留給小夥伴自行獨立思考完成。從一道面試題開始:不使用synchronized和lock,如何實現一個線程安全的單例?咱們知道,上面講過的全部方式中,只要是線程安全的,其實都直接或者間接用到了synchronized,間接用到是什麼意思呢,就好比餓漢式、靜態內部類、枚舉,其實現原理都是利用藉助了類加載的時候初始化單例,即藉助了ClassLoader的線程安全機制。
所謂ClassLoader的線程安全機制,就是ClassLoader的loadClass方法在加載類的時候使用了synchronized關鍵字。也正是由於這樣, 除非被重寫,這個方法默認在整個裝載過程當中都是同步的,也就是保證了線程安全。
那麼答案是什麼呢,就是利用CAS樂觀鎖,他雖然名字中有個鎖字,但實際上是無鎖化技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知此次競爭中失敗,並能夠再次嘗試,代碼以下:
/** * @author jack xu */
public class CASSingleton {
private static final AtomicReference<CASSingleton> INSTANCE = new AtomicReference<CASSingleton>();
private CASSingleton() {
}
public static CASSingleton getInstance() {
for (; ; ) {
CASSingleton singleton = INSTANCE.get();
if (null != singleton) {
return singleton;
}
singleton = new CASSingleton();
if (INSTANCE.compareAndSet(null, singleton)) {
return singleton;
}
}
}
}
複製代碼
在JDK1.5中新增的JUC包就是創建在CAS之上的,相對於對於synchronized這種阻塞算法,CAS是非阻塞算法的一種常見實現,他是一種基於忙等待的算法,依賴底層硬件的實現,相對於鎖它沒有線程切換和阻塞的額外消耗,能夠支持較大的並行度。雖然CAS沒有用到鎖,可是他在不停的自旋,會對CPU形成較大的執行開銷,在生產中咱們不建議使用,那麼爲何我還會講呢,由於這是工做擰螺絲,面試造火箭的典型!你能夠不用,可是你得知道,你說是吧。。
最後原創不易,若是你以爲寫的不錯,請點個贊!