單例模式與雙重檢測

轉自: http://jiangzhengjun.iteye.com/blog/652440

首先要解釋一下什麼是延遲加載,延遲加載就是等到真真使用的時候纔去創建實例,不用時不要去創建。

 

從速度和反應時間角度來講,非延遲加載(又稱餓漢式)好;從資源利用效率上說,延遲加載(又稱懶漢式)好

 

下面看看幾種常見的單例的設計方式:

 

第一種:非延遲加載單例類

Java代碼  收藏代碼
  1. public class Singleton {  
  2.  private Singleton() {}  
  3.  private static final Singleton instance = new Singleton();  
  4.  public static Singleton getInstance() {  
  5.   return instance;  
  6.  }  
  7. }  

 

第二種:同步延遲加載

Java代碼  收藏代碼
  1. public class Singleton {  
  2.  private static Singleton instance = null;  
  3.  private Singleton() {}  
  4.  public static synchronized Singleton getInstance() {  
  5.   if (instance == null) {  
  6.    instance = new Singleton();  
  7.   }  
  8.   return instance;  
  9.  }  
  10. }  

第三種:雙重檢測同步延遲加載 
爲處理原版非延遲加載方式瓶頸問題,我們需要對 instance 進行第二次檢查,目的是避開過多的同步(因爲這裏的同步只需在第一次創建實例時才同步,一旦創建成功,以後獲取實例時就不需要同獲取鎖了),但在Java中行不通,因爲同步塊外面的if (instance == null)可能看到已存在,但不完整的實例。JDK5.0以後版本若instance爲volatile則可行:

Java代碼  收藏代碼
  1. public class Singleton {  
  2.  private volatile static Singleton instance = null;  
  3.  private Singleton() {}  
  4.  public static Singleton getInstance() {  
  5.   if (instance == null) {  
  6.    synchronized (Singleton.class) {// 1  
  7.     if (instance == null) {// 2  
  8.      instance = new Singleton();// 3  
  9.     }  
  10.    }  
  11.   }  
  12.   return instance;  
  13.  }  
  14. }  

雙重檢測鎖定失敗的問題並不歸咎於 JVM 中的實現 bug,而是歸咎於 Java 平臺內存模型。內存模型允許所謂的“無序寫入”,這也是失敗的一個主要原因

 

無序寫入
爲解釋該問題,需要重新考察上述清單中的 //3 行。此行代碼創建了一個 Singleton 對象並初始化變量 instance 來引用此對象。這行代碼的問題是:在 Singleton 構造函數體執行之前,變量 instance 可能成爲非 null 的,即賦值語句在對象實例化之前調用,此時別的線程得到的是一個還會初始化的對象,這樣會導致系統崩潰。
什麼?這一說法可能讓您始料未及,但事實確實如此。在解釋這個現象如何發生前,請先暫時接受這一事實,我們先來考察一下雙重檢查鎖定是如何被破壞的。假設代碼執行以下事件序列:


1、線程 1 進入 getInstance() 方法。
2、由於 instance 爲 null,線程 1 在 //1 處進入 synchronized 塊。 
3、線程 1 前進到 //3 處,但在構造函數執行之前,使實例成爲非 null。 
4、線程 1 被線程 2 預佔。
5、線程 2 檢查實例是否爲 null。因爲實例不爲 null,線程 2 將 instance 引用返回給一個構造完整但部分初始化了的 Singleton 對象。 
6、線程 2 被線程 1 預佔。
7、線程 1 通過運行 Singleton 對象的構造函數並將引用返回給它,來完成對該對象的初始化。

 

爲展示此事件的發生情況,假設代碼行 instance =new Singleton(); 執行了下列僞代碼:
mem = allocate();             //爲單例對象分配內存空間.
instance = mem;               //注意,instance 引用現在是非空,但還未初始化
ctorSingleton(instance);    //爲單例對象通過instance調用構造函數


這段僞代碼不僅是可能的,而且是一些 JIT 編譯器上真實發生的。執行的順序是顛倒的,但鑑於當前的內存模型,這也是允許發生的。JIT 編譯器的這一行爲使雙重檢查鎖定的問題只不過是一次學術實踐而已。

 

 

如果真像這篇文章:http://dev.csdn.net/author/axman/4c46d233b388419e9d8b025a3c507b17.html所說那樣的話,1.2或以後的版本就不會有問題了,但這個規則是JMM的規範嗎?誰能夠確認一下。
確實,在JAVA2(以jdk1.2開始)以前對於實例字段是直接在主儲區讀寫的.所以當一個線程對resource進行分配空間,
初始化和調用構造方法時,可能在其它線程中分配空間動作可見了,而初始化和調用構造方法還沒有完成.

但是從JAVA2以後,JMM發生了根本的改變,分配空間,初始化,調用構造方法只會在線程的工作存儲區完成,在沒有
向主存儲區複製賦值時,其它線程絕對不可能見到這個過程.
而這個字段複製到主存區的過程,更不會有分配空間後
沒有初始化或沒有調用構造方法的可能.在JAVA中,一切都是按引用的值複製的.向主存儲區同步其實就是把線程工作
存儲區的這個已經構造好的對象有壓縮堆地址值COPY給主存儲區的那個變量.這個過程對於其它線程,要麼是resource
爲null,要麼是完整的對象.絕對不會把一個已經分配空間卻沒有構造好的對象讓其它線程可見.

 

另一篇詳細分析文章:http://www.iteye.com/topic/260515

 

第四種:使用ThreadLocal修復雙重檢測

 

藉助於ThreadLocal,將臨界資源(需要同步的資源)線程局部化,具體到本例就是將雙重檢測的第一層檢測條件 if (instance == null) 轉換爲了線程局部範圍內來作。這裏的ThreadLocal也只是用作標示而已,用來標示每個線程是否已訪問過,如果訪問過,則不再需要走同步塊,這樣就提高了一定的效率。但是ThreadLocal在1.4以前的版本都較慢,但這與volatile相比卻是安全的。

 

Java代碼  收藏代碼
  1. public class Singleton {  
  2.  private static final ThreadLocal perThreadInstance = new ThreadLocal();  
  3.  private static Singleton singleton ;  
  4.  private Singleton() {}  
  5.    
  6.  public static Singleton  getInstance() {  
  7.   if (perThreadInstance.get() == null){  
  8.    // 每個線程第一次都會調用  
  9.    createInstance();  
  10.   }  
  11.   return singleton;  
  12.  }  
  13.   
  14.  private static  final void createInstance() {  
  15.   synchronized (Singleton.class) {  
  16.    if (singleton == null){  
  17.     singleton = new Singleton();  
  18.    }  
  19.   }  
  20.   perThreadInstance.set(perThreadInstance);  
  21.  }  
  22. }  

 
第五種:使用內部類實現延遲加載
爲了做到真真的延遲加載,雙重檢測在Java中是行不通的,所以只能藉助於另一類的類加載加延遲加載:

Java代碼  收藏代碼
  1. public class Singleton {  
  2.  private Singleton() {}  
  3.  public static class Holder {  
  4.   // 這裏的私有沒有什麼意義  
  5.   /* private */static Singleton instance = new Singleton();  
  6.  }  
  7.  public static Singleton getInstance() {  
  8.   // 外圍類能直接訪問內部類(不管是否是靜態的)的私有變量  
  9.   return Holder.instance;  
  10.  }  
  11. }  

 

單例測試

下面是測試單例的框架,採用了類加載器與反射。
注,爲了測試單便是否爲真真的單例,我自己寫了一個類加載器,且其父加載器設置爲根加載器,這樣確保Singleton由MyClassLoader加載,如果不設置爲根加載器爲父加載器,則默認爲系統加載器,則Singleton會由系統加載器去加載,但這樣我們無法卸載類加載器,如果加載Singleton的類加載器卸載不掉的話,那麼第二次就不能重新加載Singleton的Class了,這樣Class不能得加載則最終導致Singleton類中的靜態變量重新初始化,這樣就無法測試了。
下面測試類延遲加載的結果是可行的,同樣也可用於其他單例的測試:

Java代碼  收藏代碼
  1. public class Singleton {  
  2.  private Singleton() {}  
  3.   
  4.  public static class Holder {  
  5.   // 這裏的私有沒有什麼意義  
  6.   /* private */static Singleton instance = new Singleton();  
  7.  }  
  8.   
  9.  public static Singleton getInstance() {  
  10.   // 外圍類能直接訪問內部類(不管是否是靜態的)的私有變量  
  11.   return Holder.instance;  
  12.  }  
  13. }  
  14.   
  15. class CreateThread extends Thread {  
  16.  Object singleton;  
  17.  ClassLoader cl;  
  18.   
  19.  public CreateThread(ClassLoader cl) {  
  20.   this.cl = cl;  
  21.  }  
  22.   
  23.  public void run() {  
  24.   Class c;  
  25.   try {  
  26.    c = cl.loadClass("Singleton");  
  27.    // 當兩個不同命名空間內的類相互不可見時,可採用反射機制來訪問對方實例的屬性和方法  
  28.    Method m = c.getMethod("getInstance"new Class[] {});  
  29.    // 調用靜態方法時,傳遞的第一個參數爲class對象  
  30.    singleton = m.invoke(c, new Object[] {});  
  31.    c = null;  
  32.    cl = null;  
  33.   } catch (Exception e) {  
  34.    e.printStackTrace();  
  35.   }  
  36.  }  
  37. }  
  38.   
  39. class MyClassLoader extends ClassLoader {  
  40.  private String loadPath;  
  41.  MyClassLoader(ClassLoader cl) {  
  42.   super(cl);  
  43.  }  
  44.  public void setPath(String path) {  
  45.   this.loadPath = path;  
  46.  }  
  47.  protected Class findClass(String className) throws ClassNotFoundException {  
  48.   FileInputStream fis = null;  
  49.   byte[] data = null;  
  50.   ByteArrayOutputStream baos = null;  
  51.   
  52.   try {  
  53.    fis = new FileInputStream(new File(loadPath  
  54.      + className.replaceAll("\\.""\\\\") + ".class"));  
  55.    baos = new ByteArrayOutputStream();  
  56.    int tmpByte = 0;  
  57.    while ((tmpByte = fis.read()) != -1) {  
  58.     baos.write(tmpByte);  
  59.    }  
  60.    data = baos.toByteArray();  
  61.   } catch (IOException e) {  
  62.    throw new ClassNotFoundException("class is not found:" + className,  
  63.      e);  
  64.   } finally {  
  65.    try {  
  66.     if (fis != null) {  
  67.      fis.close();  
  68.     }  
  69.     if (fis != null) {  
  70.      baos.close();  
  71.     }  
  72.   
  73.    } catch (Exception e) {  
  74.     e.printStackTrace();  
  75.    }  
  76.   }  
  77.   return defineClass(className, data, 0, data.length);  
  78.  }  
  79. }  
  80.   
  81. class SingleTest {  
  82.  public static void main(String[] args) throws Exception {  
  83.   while (true) {  
  84.    // 不能讓系統加載器直接或間接的成爲父加載器  
  85.    MyClassLoader loader = new MyClassLoader(null);  
  86.    loader  
  87.      .setPath("D:\\HW\\XCALLC16B125SPC003_js\\uniportal\\service\\AAA\\bin\\");  
  88.    CreateThread ct1 = new CreateThread(loader);  
  89.    CreateThread ct2 = new CreateThread(loader);  
  90.    ct1.start();  
  91.    ct2.start();  
  92.    ct1.join();  
  93.    ct2.join();  
  94.    if (ct1.singleton != ct2.singleton) {  
  95.     System.out.println(ct1.singleton + " " + ct2.singleton);  
  96.    }  
  97.    // System.out.println(ct1.singleton + " " + ct2.singleton);  
  98.    ct1.singleton = null;  
  99.    ct2.singleton = null;  
  100.    Thread.yield();  
  101.   }  
  102.  }  
  103. }  

 

發佈了17 篇原創文章 · 獲贊 2 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章