Java 單例模式

     單例模式對於頻繁使用的對象,可以省略創建對象所花費的時間,這對於那些重量級對象而言,是非常可觀的一筆系統開銷,另外由於 new 操作的次數減少,因而對系統內存的使用頻率也會降低,這將減輕 GC 壓力,縮短 GC 停頓時間。常應用於讀取配置文件,sessionFactory等場景。

非同步的單例模式代碼
public class Singleton implements Runnable{
 private static Singleton instance = new Singleton();
 public static Singleton getInsatnce(){
  return instance;
 }
}

雖然實現了單例模式,但並沒有延遲加載的效果

延遲加載的單例模式代碼

public class LazySingleton {
 
 private static LazySingleton instance = null;
 public static synchronized LazySingleton getInstance(){
 if(instance == null){
 instance = new LazySingleton();
 }
 return instance;
 }
}
由於引入了同步關鍵字,導致多線程環境下耗時明顯增加,並且99%的情況下不需要同步的。

Double Checked locking 模式

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



這段代碼看起來很完美,很可惜,它是有問題。主要在於instance = new Singleton()這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。
1.給 instance 分配內存
2.調用 Singleton 的構造函數來初始化成員變量
3.將instance對象指向分配的內存空間(執行完這步 instance 就爲非 null 了)
但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,被線程二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然後使用,然後順理成章地報錯。


我們只需要將 instance 變量聲明成 volatile 就可以了。

public class Singleton{
private volatile static Singleton instance = null;  
public static Singleton getInstance(Context context) {  
    if (instance == null) {                              
        synchronized (Singleton.class) {       
             if (instance == null) {                       
                instance = new Singleton(); 
            }
        }
    }
    return sInst;                                     
}
}
有些人認爲使用 volatile 的原因是可見性,也就是可以保證線程在本地不會存有 instance 的副本,每次都是去主內存中讀取。但其實是不對的。使用 volatile 的主要原因是其另一個特性:禁止指令重排序優化。也就是說,在 volatile 變量的賦值操作後面會有一個內存屏障(生成的彙編代碼上),讀操作不會被重排序到內存屏障之前。比如上面的例子,取操作必須在執行完 1-2-3 之後或者 1-3-2 之後,不存在執行到 1-3 然後取到值的情況。從「先行發生原則」的角度理解的話,就是對於一個 volatile 變量的寫操作都先行發生於後面對這個變量的讀操作(這裏的“後面”是時間上的先後順序)。




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