1. 程式人生 > 實用技巧 >單例模式詳解

單例模式詳解

單例模式詳解

一、單例模式分類

單例模式按照載入時間可以分為兩種:

  • 懶漢式
  • 餓漢式

二、各種單例模式詳解

2.1 餓漢式
public class Singleton {
    private static Singleton singleton = new Singleton();

    private Singleton() {
    }
    
    public static Singleton getSingleton() {
        return singleton;
    }
}

​ 餓漢式單例模式在程式執行時,物件就會被建立。每次呼叫getSingleton方法時,都會返回同一個物件。餓漢式單例不存線上程安全問題。

2.2 懶漢式
public class Singleton implements Serializable{
    private static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getSingleton() {
        if (null == singleton) {
            singleton = new Singleton();
            return singleton;
        }
        return singleton;
    }
}

​ 在單執行緒中,該單例模式沒有什麼問題,但是在多執行緒程式中,該單例模式就存線上程安全問題。當第一次有多個執行緒呼叫getSingleton時,可能會產生多個singleton物件。為了解決該問題最直觀的辦法就是在getSingleton加鎖,使在多執行緒情況下該方法序列化執行,保證執行緒安全。程式碼如下:

public class Singleton {
    private static Singleton singleton;

    private Singleton() {
    }

    public static synchronized Singleton getSingleton() {
        if (null == singleton) {
            singleton = new Singleton();
            return singleton;
        }
        return singleton;
    }
}

​ 該方法可以解決執行緒安全問題,但是由於初始化物件和每次獲取物件都要通過鎖,導致效能大大下降。而實際我們只是希望在初次初始化物件時保證執行緒安全,在之後獲取物件不存線上程安全問題,所以就產生了雙重檢查鎖。程式碼如下:

public class Singleton implements Serializable {
    private static Singleton singleton;

    private Singleton() {
    }

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

​ 但是仔細分析,該方法還是有問題,因為new Singleton()這個初始化物件可以分為三個操作,1、分配記憶體空間;2、初始化物件;3、設定例項化物件指向分配的地址。在一些JIT編譯器中,為了達到優化目的,可能會對這三個操作進行重排序。當2和3順序顛倒後,就可能發生執行緒安全問題,原因是當初始化物件後,但是物件還沒有指向分配的記憶體地址,而此時有另一個執行緒執行到if(null==singleton)這行程式碼,就會判斷為false,然後就會去獲取物件,從而發生錯誤。雖然這種機率很小,並且並不是所有的編譯器都會對上述操作進行重排序,但是該程式碼始終是存在問題的程式碼,所以就需要去解決。

重排序:重排序是指編譯器和處理器為了優化程式效能而對指令序列進行重新排序的一種手段。 --《java併發程式設計的藝術》

​ 通過對該種情況分析,有兩種解決思路:

  1. 禁止重排序
  2. 在例項化操作完成前,不允許其他執行緒看到該物件例項化的具體過程(也就是在例項化完成前,其他執行緒讀取的情況會一直為null)。

具體實現:

第一種情況我們理所當然會想到volatile關鍵字,因為它可以禁止指令重排序。具體實現程式碼如下:

public class Singleton {
	private volatile static Singleton singleton;

	private Singleton() {
	}

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

第二種方案不容易想到具體程式碼實現,《java併發程式設計的藝術》中是指可以通過靜態工廠例項化物件來實現(章節:3.8.3),具體實現程式碼:

public class Singleton {
  private volatile static Singleton singleton;

  public Singleton() {
  }
}
public class SingletonFactory {
	private static class SingletonHolder {
		public static Singleton singleton = new Singleton();
	}
	public static Singleton getSingleton() {
		return SingletonHolder.singleton;
	}
}

​ 該種方法主要是基於JVM在類的初始化階段(即在Class被載入後,且被執行緒使用之前),會執行類的初始化。在執行類的初始化期間,JVM會去獲取一個鎖,這個鎖可以同步多個執行緒對同一個類的初始化。這種方法其實和餓漢式類似,只是通過靜態工廠來實現再需要該物件時再進行載入。但是上面的程式碼並完善,因為是由兩個類來實現單例,單例物件的構造方法也是公共的(能夠new出新的物件)。因此最好將單例實現放在一個類中,並能達到懶載入的效果,程式碼如下:

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

	private Singleton () {}
	public static Singleton getSingleton() {
		return SingletonHolder.INSTANCE;
	}
}
2.3 列舉實現單例

​ 嚴格的來說,列舉也是懶漢式單例,因為只有在列舉類被呼叫後才會例項化該單例。具體程式碼如下:

public enum Singleton {
    INSTANCE;

    public void doEverything() {
    }
}

列舉類實現的單例不僅是執行緒安全的,而且不會被序列化和反射破壞,這是其他形式的單例不能達到的,這也是列舉實現單例備受推崇的原因。

三、破壞單例模式的各種情況

​ 單例能夠減少重複建立物件,提高效能。但是單例有時是能夠被破壞的,下面我們來了解單例可能被破壞的情況。

3.1 反射破壞單例
public class Singleton {
    private static Singleton singleton = new Singleton();

    private Singleton() {
    }

    public static Singleton getSingleton() {
        return singleton;
    }
}

public class DestroySingleton {
    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("com.yuan.Singleton");
        Constructor con = clazz.getDeclaredConstructor();
        con.setAccessible(true);
        Singleton singleton1 = (Singleton) con.newInstance();
        Singleton singleton2 = Singleton.getSingleton();
        System.out.println(singleton1 == singleton2);
    }
}

輸出結果:false

​ 結果表明,通過反射將會例項化一個新的物件,從而破壞單例模式。但是當採用列舉實現單例時,由於列舉類實現反射會報錯,從而可以避免單例破壞。

3.2 序列化破壞單例
public class Singleton implements Serializable{
    private static Singleton singleton = new Singleton();

    private Singleton() {
    }

    public static Singleton getSingleton() {
        return singleton;
    }
}

public class DestroySingleton {
    public static void main(String[] args) throws Exception {
        //write Object
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\file.txt"));
        oos.writeObject(Singleton.getSingleton());
        //read Object
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\file.txt"));
        Singleton singleton = (Singleton) ois.readObject();
        System.out.println(singleton == Singleton.getSingleton());
    }
}

輸出結果:false

​ 通過程式碼知道,序列化會例項化一個新物件。但是當單例物件新增readResolve方法時,可以避免單例被破壞

立即載入:

public class Singleton implements Serializable{
    private static Singleton singleton = new Singleton();

    private Singleton() {
    }

    public static Singleton getSingleton() {
        return singleton;
    }

    /**
     * 新增該方法可以避免單例被破壞
     */
    public Object readResolve() {
        return Singleton.getSingleton();
    }
}

public class DestroySingleton {
    public static void main(String[] args) throws Exception {
        //write Object
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\file.txt"));
        oos.writeObject(Singleton.getSingleton());
        //read Object
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\file.txt"));
        Singleton singleton = (Singleton) ois.readObject();
        System.out.println(singleton == Singleton.getSingleton());
    }
}

輸出結果:true

懶載入:

public class Singleton implements Serializable{
    private volatile static Singleton singleton;

    private Singleton() {
    }

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

    /**
     * 新增該方法可以避免單例被破壞
     */
    public Object readResolve() {
        return Singleton.getSingleton();
    }
}

public class DestroySingleton {
    public static void main(String[] args) throws Exception {
        //write Object
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\file.txt"));
        oos.writeObject(Singleton.getSingleton());
        //read Object
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\file.txt"));
        Singleton singleton = (Singleton) ois.readObject();
        System.out.println(singleton);
        System.out.println(Singleton.getSingleton());
        System.out.println(singleton == Singleton.getSingleton());
    }
}

​ 其實就是java約定,在例項化後會再判斷該例項是否有該方法,如果有該方法則會呼叫該方法將返回結果返回。這裡要注意的是立即載入模式下readResolve方法可以直接返回singleton這個類變數,但是在懶載入中這種做法不建議,因為如果程式啟動沒有獲取過對像例項而是直接通過序列化來獲得,就會返回一個空值。

四、總結

​ 單例實現的方式有很多,有執行緒安全也有不安全的,所以需要根據實際業務具體分析,再決定採用。主要考慮的併發、反射以及序列化。