/**
*
* @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 件事情。
- 給 instance 分配內存
- 調用 Singleton 的構造函數來初始化成員變量
- 將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,而且還能防止反序列化導致重新創建新的對象。