[摘要] 單例模式是一種常見的設計模式,在Java應用中,單例對象能保證在一個JVM中,該對象只有一個實例存在。正是因爲這個特色,單例對象一般做爲程序中的存放配置信息的載體,由於它能保證其餘對象讀到一致的信息。這種方式只需訪問該單例對象便可達到統一可是在多線程環境下,可是隨着應用場景的不一樣,也可能帶來一些同步問題。 面試
本文將探討一下在多線程環境下,使用單例對象時可能會帶來的同步問題,並給出可選的解決辦法。設計模式
[關鍵字] Java 設計模式 單例 線程 同步 雙重檢查鎖安全
[概念]多線程
單例模式分類:懶漢式單例、餓漢式單例兩種。
單例模式特色:
1、單例類只能有一個實例
2、單例類必須本身本身建立本身的惟一實例
3、單例類必須給全部其餘對象提供這一實例併發
單例模式確保某個類只有一個實例,並且自行實例化並向整個系統提供這個實例性能
[問題描述]測試
面試的時候,你們也許會被問到這樣一個問題:請您寫出一個單例模式(Singleton Pattern) ,固然都感受比較簡單,代碼以下:spa
/**.net
* 演示單例模式之飢餓模式線程
* @author Administrator
*
*/
public class EagerSingleton
{
private static EagerSingleton instance=new EagerSingleton();
private EagerSingleton()
{
}
public static EagerSingleton getSingleInstance()
{
return instance;
}
}
這種寫法就是所謂的飢餓模式,每一個對象在沒有使用以前就已經初始化了。這就可能帶來潛在的性能問題:若是這個對象很大呢?沒有使用這個對象以前,就把它加載到了內存中去是一種巨大的浪費。針對這種狀況,咱們能夠對以上的代碼進行改進,使用一種新的設計思想——延遲加載(Lazy-load Singleton)。
/**
* 演示單例模式之懶漢模式
* @author Administrator
*
*/
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton()
{
}
public static LazySingleton getSingleInstance()
{
if (instance == null)
{
instance = new LazySingleton();
}
return instance;
}
}
這種寫法就是所謂的懶漢模式。它使用了延遲加載來保證對象在沒有使用以前,是不會進行初始化的。可是,一般這個時候面試官又會提問新的問題來刁難一下。他會問:這種寫法線程安全嗎?回答必然是:不安全。
測試結果:
這是由於在多個線程可能同時運行到判斷instance 爲null,因而同時進行了初始化。因此,這是面臨的問題是如何使得這個代碼線程安全?很簡單,在那個方法前面加一個Synchronized就OK了。
/**
* 演示單例模式之線程安全
* @author Administrator
*
*/
public class ThreadSafeSingleton
{
private static ThreadSafeSingleton instance;
private ThreadSafeSingleton()
{
}
public static synchronized ThreadSafeSingleton getSingleInstance()
{
if (instance == null)
{
instance = new ThreadSafeSingleton();
}
return instance;
}
}
寫到這裏,面試官可能仍然會狡猾的看了你一眼,繼續刁難到:這個寫法有沒有什麼性能問題呢?答案確定是有的!同步的代價必然會必定程度的使程序的併發度下降。那麼有沒有什麼方法,一方面是線程安全的,有能夠有很高的併發度呢?咱們觀察到,線程不安全的緣由實際上是在初始化對象的時候,因此,能夠想辦法把同步的粒度下降,只在初始化對象的時候進行同步。
[解決方案]
這裏有必要提出一種新的設計思想——雙重檢查鎖(Double-Checked Lock)。
/**
* 演示單例模式之雙重鎖定
* @author Administrator
*
*/
public class DoubleCheckedSingleton {
private static DoubleCheckedSingleton instance;
private DoubleCheckedSingleton()
{
}
public static synchronized DoubleCheckedSingleton getSingleInstance()
{
//性能改進——雙重鎖定: Double-Check Locking
if(instance==null) // 1. 先判斷
{
synchronized (DoubleCheckedSingleton.class) // 2. 再同步
{
if (instance == null) //3. 再判斷
{
instance = new DoubleCheckedSingleton(); //4. 實例化
}
}
}
return instance;
}
}
這種寫法使得只有在加載新的對象進行同步,在加載完了以後,其餘線程就能夠判斷當前實例對象是否爲空,如非空,並跳過鎖的的代價直接返回當前單例對象了。作到很好的併發度。
至此,上面的寫法一方面實現了Lazy-Load,另外一個方面也作到了併發度很好的線程安全,一切看上很完美。
這是,面試官可能會對你的回答滿意的點點頭
可是,當你此時提出說,其實這種寫法仍是有問題的!面試官也許會對你另眼相看!!
問題在哪裏?假設線程A執行到調用上述getSingleInstance()方法,它判斷對象爲空,因而線程A執行下面初始化這個對象,但初始化是須要耗費時間的,可是這個對象的地址其實已經存在了。此時若是線程B也執行調用上述getSingleInstance()方法,它判斷不爲空,因而直接跳到最後,返回獲得了這個對象。可是,這個對象還沒有被完整的初始化!獲得一個沒有完全初始化徹底的對象有什麼用!!
關於這個Double-Checked Lock的討論有不少,目前公認這是一個Anti-Pattern(即:反面模式),不推薦使用!因此當這個面試官聽到你的這番答覆,他會不會被Hold不住呢?
那麼有沒有什麼更好的寫法呢?
有!這裏又要提出一種新的模式——Initialization on Demand Holder. 這種方法使用內部類來作到延遲加載對象,在初始化這個內部類的時候,JLS(Java Language Sepcification)會保證這個類的線程安全。這種寫法最大的巧妙在於,徹底使用了Java虛擬機的機制進行同步保證,沒有一個同步的關鍵字。
/**
* 演示單例模式之完美實現
* @author Administrator
*
*/
public class Singleton
{
private static class SingletonHolder
{
public final static Singleton instance = new Singleton();
}
public static Singleton getInstance()
{
return SingletonHolder.instance;
}
}
測試結果:
單個線程
多線程
至此,單例模式以及線程安全,咱們作了一個系統的比較,但願對你有所幫助!