public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
線程安全問題:當多線程同時調用getInstance()方法,同時判斷出instance,進入實例化操作,單利就不復存在。
為了線程安全,那我們對getInstance()方法進行同步化:
public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
synchronized修飾保證同一時間只有一個線程可以執行getInstance方法,如此可以保證單例的線程安全。但是同步粒度似乎太大,事實上當實例初始化後我們並不需保證一個個線程排隊來取這個實例。
那麼就引出來雙重檢驗鎖的代碼:public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getSingleton() { if (instance == null) { //Single Checked synchronized (this) { if (instance == null) { //Double Checked instance = new Singleton(); } } } return instance ; } }
同步快外面和裡面check兩次,保證在進入同步塊的線程不產生新的實例。
當多個線程並發時都判斷出實例為null則開始競爭鎖,其中一個線程拿到鎖去實例化操作,而其他線程需要等待,而實例化好的線程釋放所後,後進入的線程如果不判斷單例是否已經產生,那麼就會又去實例化一個對象,如此就不能實現單例了。 如此雙重檢驗鎖開啟來完美,而指令重排序會引起問題。我想這也是一個學習重排序的好例子。instance = new Singleton();
上面這個代碼不是一個原子操作,即無法被翻譯成一個指令完成。
它是由一下3個指令完成的:給 instance 分配內存 調用 Singleton 的構造函數來初始化成員變量 將instance對象指向分配的內存空間地址
JVM編譯時進行指令重排序可能打亂上面三個指令的執行順序,也就是說可能先直行來1,3然後執行2。那麼有這麼一種情況當執行好1和3,instance不為null,新進入的線程在判斷第一個null時就會直接返回一個沒有執行2步驟的實例,如此就有不符合期望了。這的確是個經典的場景。
額外閱讀
如果我們在實例初始化後,將第三步,分開寫,似乎可以解決這個問題,代碼如下:
public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { Singleton temp = instance; if (temp == null) { synchronized (Singleton.class) { temp = new Singleton(); } instance = temp; } } } return instance; }volatile關鍵字事實上是對編譯時的重排序進行了屏障。具體各家說法可以閱讀下面的文章:
擴展閱讀
由上可以感受到,在累加載時就初始化好實例,會有很多需要考慮的東西,那麼如果在編譯階段就實例化好,如此就可以避免並發帶來的問題。
那就是所謂的餓漢式單例:
public class Singleton{ //類加載時就初始化 private static final Singleton instance = new Singleton(); private Singleton(){} public static Singleton getInstance(){ return instance; } }
當然這樣做自然有自己的弊端,就是這個單例在沒有被使用到的時候就已經需要實例化出來,如此就會占用無謂占用內存,如果這個實例初始化復雜占用資源,而實際未必會使用就比較尴尬了。
或者說,這種方式實例化將無法實現依賴外部參數實例化的場景。
還有一種推薦寫法:
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
還有一種大師推薦的寫法,有沒有很高大上:
public enum EasySingleton{ INSTANCE; }
我們來看看如何破壞單例:
1,序列化與反序列化 當然我們前面寫的代碼不需要序列化和反序列化,就沒有這個問題了,只是說送這個方面我們考慮如何破壞它,參考如下例子:public class Singleton implements Serializable{ private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } } public class SerializableDemo1 { //為了便於理解,忽略關閉流操作及刪除文件操作。真正編碼時千萬不要忘記 //Exception直接拋出 public static void main(String[] args) throws IOException, ClassNotFoundException { //Write Obj to file ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile")); oos.writeObject(Singleton.getSingleton()); //Read Obj from file File file = new File("tempFile"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); Singleton newInstance = (Singleton) ois.readObject(); //判斷是否是同一個對象 System.out.println(newInstance == Singleton.getSingleton()); } } //false
2,反射
public class Singleton { public static final Singleton INSTANCE = new Singleton(); private Singleton() { } public Singleton getInstance() { return INSTANCE; } public static void main(String[] args) throws Exception { // 反射機制破壞單例模式 Class clazz = Singleton.class; Constructor c = clazz.getDeclaredConstructor(); // 反射機制使得private方法可以被訪問!!! c.setAccessible(true); // 判斷反射生成的對象與單例對象是否相等 System.out.println(Singleton.INSTANCE == c.newInstance()); } }
破壞單例的原理就是,不走構造函數即可產生實例的方式,因為我們只關閉了構造函數。
至此,對java單例有一個比較全面的認識,牽涉到大量知識點,需要繼續挖掘。
讓我們繼續前行
----------------------------------------------------------------------
努力不一定成功,但不努力肯定不會成功。