單例模式(Singleton)的分析----基於java語言

單例模式(Singleton)

     

 單例對象(Singleton)是一種常用的設計模式。在Java應用中,單例對象能保證在一個JVM中,該對象只有一個實例存在。這樣的模式有幾個好處:

    1、某些類創建比較頻繁,對於一些大型的對象,這是一筆很大的系統開銷。
    2、省去了new操作符,降低了系統內存的使用頻率,減輕GC壓力。
    3、有些類如交易所的核心交易引擎,控制着交易流程,如果該類可以創建多個的話,系統完全亂了。

   //簡單的單例類 餓漢模式:

public class Singleton {  

		/* 持有私有靜態實例,防止被引用*/  
		private static Singleton instance = new Singleton();  

		/* 私有構造方法,防止被實例化 */  
		private Singleton() {  
		}  

		/* 靜態工程方法,返回Singleton實例 */  
		public static Singleton getInstance() {  
		    return instance;  
		}  

		/* 如果該對象被用於序列化,可以保證對象在序列化前後保持一致 */  
		private Object readResolve() {  
		    return instance;  
		}  
	}  


    //    這個類是可以實現單例模式的,但是存在不少問題,比如在類中不管用戶是否要使用該類的對象,就先創建好了一個實例放在內存中,這樣浪費了內存,因此我們想到在靜態方法中來創建對象。

    //簡單的單例類 懶漢模式:

  
  public class Singleton {  

        /* 持有私有靜態實例,防止被引用,此處賦值爲null,目的是實現延遲加載 */  
        private static Singleton instance = null;  

        /* 私有構造方法,防止被實例化 */  
        private Singleton() {  
        }  

        /* 靜態工程方法,創建實例 */  
        public static Singleton getInstance() {  
            if (instance == null) {  
                instance = new Singleton();  
            }  
            return instance;  
        }  

        /* 如果該對象被用於序列化,可以保證對象在序列化前後保持一致 */  
        private Object readResolve() {  
            return instance;  
        }  
    }  



    這個類可以滿足基本要求,但是,像這樣毫無線程安全保護的類,如果我們把它放入多線程的環境下,肯定就會出現問題了,當多線程時,有可能一個線程正在創建對象但沒有創建完成,另一個線程又通過if(instance==null)進入來創建對象,這樣違反了單例模式.如何解決?我們首先會想到對getInstance方法加synchronized關鍵字,如下:

 加鎖:
   public static synchronized Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }      


    但是,synchronized作爲修飾符在方法上使用,在性能上會有所下降,因爲每次調用getInstance(),都要對對象上鎖,事實上,只有在第一次創建對象的時候需要加鎖,之後就不需要了,所以,這個地方需要改進。我們改成下面這個:

內部鎖:

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



    似乎解決了之前提到的問題,將synchronized關鍵字加在了方法內部,也就是說當調用的時候是不需要加鎖的,只有在instance爲null,並創建對象的時候才需要加鎖,性能有一定的提升。但是,這樣的情況,還是有可能有問題的。
    看下面的情況:在Java指令中創建對象和賦值操作是分開進行的,也就是說instance = new Singleton();語句並非是一個原子操作,在 JVM 中這句代碼大概做了下面 3 件事情:
        1給 new的對象 分配內存
        2調用 Singleton 的構造函數來初始化成員變量
        3將引用instance指向分配的內存空間(執行完這步 instance 就爲非 null 了)

    但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,另外一個線程B搶奪到了CPU的執行權,這時instance已經是非null了(但卻沒有初始化),所以線程B會直接返回 instance,然後使用,結果就會出現問題了(因爲對象還沒有初始化)。

    還有另外一種解決方案:使用內部類來維護單例的實現,JVM內部的機制能夠保證當一個類被加載的時候,這個類的加載過程是線程互斥的(就是加載完畢後別的線程才能使用)。這樣當我們第一次調用getInstance的時候,JVM能夠幫我們保證instance只被創建一次,並且會保證把賦值給instance的內存初始化完畢,這樣我們就不用擔心上面的問題。同時該方法也只會在第一次調用的時候使用互斥機制,這樣就解決了低性能問題。

靜態內部類:

    
    public class Singleton {  
      
        /* 私有構造方法,防止被實例化 */  
        private Singleton() {  
        }  

        /* 此處使用一個內部類來維護單例 */  
        private static class SingletonFactory {  
            private static Singleton instance = new Singleton();  
        }  

        /* 獲取實例 */  
        public static Singleton getInstance() {  
            return SingletonFactory.instance;  
        }  

        /* 如果該對象被用於序列化,可以保證對象在序列化前後保持一致 */  
        private Object readResolve() {  
            return getInstance();  
        }  
    }



    但是如果在構造函數中拋出異常,實例將永遠得不到創建,也會出錯。所以說,十分完美的東西是沒有的,我們只能根據實際情況,選擇最適合自己應用場景的實現方法。

    這樣做就完美了嗎?並不是,我們還可以使用反射去創建這個類的對象,即使它的構造器是私有的,我們也是可以調用到的。那麼這個時候我們就需要再次修改代碼去訪問別人反射調用構造器.

  
反射來破壞單例:

Class<? > c = instance.getClass();
        Constructor<?> Constructors = c.getDeclaredConstructor();
        Constructors.setAccessible(true);
        Singleton5 ss = (Singleton5) Constructors.newInstance();

仍然可以創建實例.

那麼我們就會想到給他的構造器添加判斷值:

 
   //在構造器中控制一下,構造器只允許調用一次,之後在調用就拋出異常
    public class Singleton {  
        private volatile static boolean flag;
        /* 私有構造方法,防止被實例化 */  
        private Singleton() {  
            if(!flag){
                flag = false;
            }else{
                throw new RuntimeException("不能多次創建單例對象");
            }
        }  

        /* 此處使用一個內部類來維護單例 */  
        private static class SingletonFactory {  
            private static Singleton instance = new Singleton();
        }  

        /* 獲取實例 */  
        public static Singleton getInstance() {  
            return SingletonFactory.instance;  
        }  

        /* 如果該對象被用於序列化,可以保證對象在序列化前後保持一致 */  
        private Object readResolve() {  
            return getInstance();  
        }  
    }      


    反射的問題處理完了之後,這裏還有一個問題,就是如果把單例對象進行序列化然後再反序列化,那麼內存中就會出現倆個一樣的單例對象,只是內存地址不同。這種情況我們可以使用readResolve方法來防止。
    private Object readResolve(){.....}
    ObjectInputStream 會檢查對象的class是否定義了readResolve方法。如果定義了,將由readResolve方法指定返回的對象。返回對象的類型一定要是兼容的,否則會拋出ClassCastException 。
   加readResolve方法來防止反編譯:
    public abstract class Singleton8 implements Serializable{  

        private static final long serialVersionUID = 7863921642928237696L;

        /* 此處使用一個內部類來維護單例 */  
        private static class SingletonFactory {  
            @SuppressWarnings("serial")
            private static Singleton8 instance = new Singleton8(){};
        }  
        
        //測試方式,把單例對象序列化後再反序列化從而獲得一個新的對象 就相當於複製了一個單例對象
        public Singleton8 copy() throws Exception{  
                ByteArrayOutputStream os = new ByteArrayOutputStream();  
                ObjectOutputStream oos = new ObjectOutputStream(os);  
                oos.writeObject(Singleton8.getInstance());  
                  
                InputStream is = new ByteArrayInputStream(os.toByteArray());  
                ObjectInputStream ois = new ObjectInputStream(is);  
                Singleton8 obj = (Singleton8) ois.readObject();  
                return obj;  
        }
        
        /* 獲取實例 */  
        public static Singleton8 getInstance() {  
            return SingletonFactory.instance;  
        }  

        /* 如果該對象被用於序列化,可以保證對象在序列化前後保持一致 */  
        /* 把這個方法註釋前和註釋後來運行測試代碼觀察結果 */  
        private Object readResolve() {  
            return getInstance();  
        }  

    }

這樣基本達到了完美的單例狀態.

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