JAVA單例模式 關於延遲加載問題

設計模式 html

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

 

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

 

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

 

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

Java代碼  收藏代碼測試

    public class Singleton {  
     private Singleton() {}  
     private static final Singleton instance = new Singleton();  
     public static Singleton getInstance() {  
      return instance;  
     }  
    }  

第二種:同步延遲加載this

Java代碼  收藏代碼spa

    public class Singleton {  
     private static Singleton instance = null;  
     private Singleton() {}  
     public static synchronized Singleton getInstance() {  
      if (instance == null) {  
       instance = new Singleton();  
      }  
      return instance;  
     }  
    }  

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

Java代碼  收藏代碼線程

public class Singleton {  
 private volatile static Singleton instance = null;  
 private Singleton() {}  
 public static Singleton getInstance() {  
  if (instance == null) {  
   synchronized (Singleton.class) {// 1  
    if (instance == null) {// 2  
     instance = new Singleton();// 3  
    }  
   }  
  }  
  return instance;  
 }  
} 

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

 

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


一、線程 1 進入 getInstance() 方法。
二、因爲 instance 爲 null,線程 1 在 //1 處進入 synchronized 塊。
三、線程 1 前進到 //3 處,但在構造函數執行以前,使實例成爲非 null。
四、線程 1 被線程 2 預佔。
五、線程 2 檢查實例是否爲 null。由於實例不爲 null,線程 2 將 instance 引用返回給一個構造完整但部分初始化了的 Singleton 對象。
六、線程 2 被線程 1 預佔。
七、線程 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代碼  收藏代碼

    public class Singleton {  
     private static final ThreadLocal perThreadInstance = new ThreadLocal();  
     private static Singleton singleton ;  
     private Singleton() {}  
       
     public static Singleton  getInstance() {  
      if (perThreadInstance.get() == null){  
       // 每一個線程第一次都會調用  
       createInstance();  
      }  
      return singleton;  
     }  
      
     private static  final void createInstance() {  
      synchronized (Singleton.class) {  
       if (singleton == null){  
        singleton = new Singleton();  
       }  
      }  
      perThreadInstance.set(perThreadInstance);  
     }  
    }  

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

Java代碼  收藏代碼

    public class Singleton {  
     private Singleton() {}  
     public static class Holder {  
      // 這裏的私有沒有什麼意義  
      /* private */static Singleton instance = new Singleton();  
     }  
     public static Singleton getInstance() {  
      // 外圍類能直接訪問內部類(不論是否是靜態的)的私有變量  
      return Holder.instance;  
     }  
    }  

單例測試

 

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

Java代碼  收藏代碼

    public class Singleton {  
     private Singleton() {}  
      
     public static class Holder {  
      // 這裏的私有沒有什麼意義  
      /* private */static Singleton instance = new Singleton();  
     }  
      
     public static Singleton getInstance() {  
      // 外圍類能直接訪問內部類(不論是否是靜態的)的私有變量  
      return Holder.instance;  
     }  
    }  
      
    class CreateThread extends Thread {  
     Object singleton;  
     ClassLoader cl;  
      
     public CreateThread(ClassLoader cl) {  
      this.cl = cl;  
     }  
      
     public void run() {  
      Class c;  
      try {  
       c = cl.loadClass("Singleton");  
       // 當兩個不一樣命名空間內的類相互不可見時,可採用反射機制來訪問對方實例的屬性和方法  
       Method m = c.getMethod("getInstance", new Class[] {});  
       // 調用靜態方法時,傳遞的第一個參數爲class對象  
       singleton = m.invoke(c, new Object[] {});  
       c = null;  
       cl = null;  
      } catch (Exception e) {  
       e.printStackTrace();  
      }  
     }  
    }  
      
    class MyClassLoader extends ClassLoader {  
     private String loadPath;  
     MyClassLoader(ClassLoader cl) {  
      super(cl);  
     }  
     public void setPath(String path) {  
      this.loadPath = path;  
     }  
     protected Class findClass(String className) throws ClassNotFoundException {  
      FileInputStream fis = null;  
      byte[] data = null;  
      ByteArrayOutputStream baos = null;  
      
      try {  
       fis = new FileInputStream(new File(loadPath  
         + className.replaceAll("\\.", "\\\\") + ".class"));  
       baos = new ByteArrayOutputStream();  
       int tmpByte = 0;  
       while ((tmpByte = fis.read()) != -1) {  
        baos.write(tmpByte);  
       }  
       data = baos.toByteArray();  
      } catch (IOException e) {  
       throw new ClassNotFoundException("class is not found:" + className,  
         e);  
      } finally {  
       try {  
        if (fis != null) {  
         fis.close();  
        }  
        if (fis != null) {  
         baos.close();  
        }  
      
       } catch (Exception e) {  
        e.printStackTrace();  
       }  
      }  
      return defineClass(className, data, 0, data.length);  
     }  
    }  
      
    class SingleTest {  
     public static void main(String[] args) throws Exception {  
      while (true) {  
       // 不能讓系統加載器直接或間接的成爲父加載器  
       MyClassLoader loader = new MyClassLoader(null);  
       loader  
         .setPath("D:\\HW\\XCALLC16B125SPC003_js\\uniportal\\service\\AAA\\bin\\");  
       CreateThread ct1 = new CreateThread(loader);  
       CreateThread ct2 = new CreateThread(loader);  
       ct1.start();  
       ct2.start();  
       ct1.join();  
       ct2.join();  
       if (ct1.singleton != ct2.singleton) {  
        System.out.println(ct1.singleton + " " + ct2.singleton);  
       }  
       // System.out.println(ct1.singleton + " " + ct2.singleton);  
       ct1.singleton = null;  
       ct2.singleton = null;  
       Thread.yield();  
      }  
     }  
    }  
相關文章
相關標籤/搜索