Java單例模式幾種寫法

/**
 *
 * @author Fernando
 *    餓漢式單例
 */
public class Singleton {
    private static Singleton ins = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return ins;
    }
}

餓漢式提前實例化,沒有懶漢式中多線程問題,但不管我們是不是調用getInstance()都會存在一個實例在內存中。

採用內部類式單例類

public class Singleton {  
    private static class SingletonHolder {  
    private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    }  
} 

內部類式中,實現了延遲加載,只有我們調用了getInstance(),纔會創建唯一的實例到內存中.並且也解決了懶漢式中多線程的問題.解決的方式是利用了Classloader的特性.由於 SingletonHolder 是私有的,除了 getInstance() 之外沒有辦法訪問它,因此它是懶漢式的;



package singleton;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * 
 * @author Fernando 懶漢式單例
 */
public class Singleton1 {
	private static Singleton1 ins;
	private static int count = -1;

	private Singleton1() {

		System.out.println("count:" + (++count));

	}

	public static synchronized Singleton1 getInstance1() {//注意!這裏如果不使用同步方法,當兩個線程A和B同時進到這個函數,
                                                              //就會生成兩個實例對象,因爲此時ins == null。
		if (ins == null)
			ins = new Singleton1();
		return ins;
	}

       //這樣寫程序不會出錯,因爲整個getInstance1是一個整體的"critical section",但就是效率很不好,
       //因爲我們的目的其實只是在第一個初始化instance的時候需要locking(加鎖),而後面取用instance的時候,根本不需要線程同步

	/**
	 * 使用單例提供的getInstance()方法只能得到同一個單例,除非是使用反射方式,將會得到新的單例
	 */
	public static void main(String[] args) throws NoSuchMethodException,
			SecurityException, InstantiationException, IllegalAccessException,
			IllegalArgumentException, InvocationTargetException {
		Singleton1 ins = Singleton1.getInstance1();
		Singleton1 ins1 = Singleton1.getInstance1();

		try {
			Class c = Class.forName(Singleton1.class.getName());
			Constructor ct = c.getDeclaredConstructor();
			ct.setAccessible(true);
			Singleton1 ins2 = (Singleton1) ct.newInstance();
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

雙檢鎖寫法:

    public class Singleton{  
      private static Singleton single;    //聲明靜態的單例對象的變量  
      private Singleton(){}    //私有構造方法   
        
      public static Singleton getSingle(){    //外部通過此方法可以獲取對象    
        if(single == null){     
            synchronized (Singleton.class) {   //保證了同一時間只能只能有一個對象訪問此同步塊        
                if(single == null){      
                    single = new Singleton();          
            }     
          }  
        }    
        return single;   //返回創建好的對象   
      }  
    }  

這段代碼看起來很完美,很可惜,它是有問題。主要在於single = 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 single; //聲明成 volatile
    private Singleton (){}

    public static Singleton getSingleton() {
        if (single == null) {                         
            synchronized (Singleton.class) {
                if (single == null) {       
                    single = new Singleton();
                }
            }
        }
        return instance;
    }
   
}

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


用枚舉寫單例實在太簡單了!這也是它最大的優點。下面這段代碼就是聲明枚舉實例的通常做法。

public enum EasySingleton{
    INSTANCE;
}
我們可以通過EasySingleton.INSTANCE來訪問實例,這比調用getInstance()方法簡單多了。創建枚舉默認就是線程安全的,所以不需要擔心double checked locking,而且還能防止反序列化導致重新創建新的對象。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章