首頁 > 軟體

Java超詳細講解設計模式之一的單例模式

2022-03-26 19:01:01

單例模式

單例模式顧名思義就是單一的範例,涉及到一個單一的類,該類負責建立自己的物件,同時確保只有一個物件被建立,並且提供一種可以存取這個物件的方式,可以直接存取,不需要範例化該類的物件。

單例模式的特點:

1.單例類只能有一個範例

2.這個範例必須由單例類自己建立

3.單例類需要提供給外界存取這個範例

單例模式的作用:

單例模式主要為了保證在Java應用程式中,一個類只有一個範例存在。

1.單例模式的結構

單例模式主要有以下角色:

  • 單例類

只能建立一個範例的類

  • 存取類

測試類,就是使用單例類的類

2.單例模式的實現

2.1餓漢式

餓漢式:類載入時建立該單範例類物件

1.餓漢式-方式1 靜態成員變數

建立 餓漢式靜態成員變數 單例類

public class Demo1 {
 
    /**
     *私有構造方法  讓外界不能建立該類物件
     */
    private Demo1(){}

    /**
     * 在類中建立該本類物件 static是由於外界獲取該類物件的方法getInstance()是 static
     * 這個物件instance就是靜態成員變數
     */
    private static Demo1 instance = new Demo1();

    /**
     * 提供一個公共的存取方式,讓外界可以獲取該類的物件 static是因為外界不需要建立物件,直接通過類存取
     */
    public static Demo1 getInstance(){
        return instance;
    }
}

建立 餓漢式靜態成員變數 測試類(存取類)

public class Test1 {
    public static void main(String[] args) {
      //建立demo1類的物件 這個時候就無法通過new建立了,因為demo1的構造方法是私有的
        Demo1 instance = Demo1.getInstance();

        Demo1 instance1 = Demo1.getInstance();

        //判斷兩個物件是否是同一個
        System.out.println(instance == instance1);
        
    }
}

輸出true 表明是同一個物件,指向同一塊記憶體地址,這樣我們就保證了Demo1單例類只有一個物件被建立

2.餓漢式-方式2 靜態程式碼塊

建立 餓漢式靜態程式碼塊 單例類

public class Demo2 {
    //餓漢式單例類  靜態程式碼塊

    /**
     *私有構造方法  讓外界不能建立該類物件
     */
    private Demo2(){}

    /**
     *  宣告一個靜態的成員變數instance但是不賦值(不建立物件)
     *  沒有為instance賦值,預設為null
     */
    private static  Demo2 instance;

    /**
     * 在靜態程式碼快中為instance賦值(建立物件)
     */
    static {
        instance = new Demo2();
    }
    /**
     * 提供一個公共的存取方式,讓外界可以獲取該類的物件 static是因為外界不需要建立物件,直接通過類存取
     */
    public static Demo2  getInstance(){
        return instance;
    }
}

建立 餓漢式靜態程式碼塊 測試類

public class Test2 {
    public static void main(String[] args) {
        Demo2 instance = Demo2.getInstance();

        Demo2 instance1 = Demo2.getInstance();

        System.out.println(instance == instance1);
    }
}

輸出true 表明是同一個物件,指向同一塊記憶體地址,這樣我們就保證了Demo2單例類只有一個物件被建立

3.餓漢式-方式3(列舉方式)

列舉類實現單例模式是十分推薦的一種單例實現模式,由於列舉型別是執行緒安全的,並且只會載入一次,這是十分符合單例模式的特點的,列舉的寫法很簡單,而且列舉方式是所有單例實現中唯一一個不會被破環的單例實現模式

單例類

//列舉方式建立單例
public enum Singleton {
     INSTANCE;
}

測試類

public class Test1 {
    public static void main(String[] args) {
    Singleton instance = Singleton.INSTANCE;
    Singleton instance1 = Singleton.INSTANCE;


        System.out.println(instance == instance1);
        //輸出 true
        
    }
}

注意:

​ 由於列舉方式是餓漢式,因此根據餓漢式的特點,列舉方式也會造成記憶體浪費,但是在不考慮記憶體問題下,列舉方式是首選,畢竟實現最簡單了

2.2懶漢式

懶漢式:類載入時不會建立該單範例物件,首次使用該物件時才會建立

1.懶漢式-方式1 (執行緒不安全)

public class Demo3 {
    /**
     *私有構造方法  讓外界不能建立該類物件
     */
    private Demo3(){}

    /**
     * 在類中建立該本類物件 static是由於外界獲取該類物件的方法getInstance()是 static
     * 沒有進行賦值(建立物件)
     */
    private static  Demo3 instance;


    /**
     * 提供一個公共的存取方式,讓外界可以獲取該類的物件 static是因為外界不需要建立物件,直接通過類存取
     */
    public static Demo3 getInstance(){
        //在首次使用該物件時建立,因此instance賦值也就是物件建立 就是在外界獲取該單例類的方法getInstance()中建立
        instance = new Demo3();
        return instance;
    }

}
public class Test3 {
    public static void main(String[] args) {
        Demo3 instance = Demo3.getInstance();

        Demo3 instance1 = Demo3.getInstance();
        //判斷兩個物件是否是同一個
        System.out.println(instance == instance1);
    }
}

輸出結果為false,表明我們建立懶漢式單例失敗了。是因為我們在呼叫getInstance()時每次呼叫都會new一個範例物件,那麼也就必然不可能相等了。

   // 如果instance為null,表明還沒有建立該類的物件,那麼就進行建立
        if(instance == null){
          instance = new Demo3();
        }
        //如果instance不為null,表明已經建立過該類的物件,根據單例類只能建立一個物件的特點,因此         //我們直接返回instance
        return instance;
    }

注意:

我們在測試是隻是單執行緒,但是在實際應用中必須要考慮到多執行緒的問題。我們假設一種情況,執行緒1進入if判斷然後還沒來得及建立instance,這個時候執行緒1失去了cpu的執行權變為阻塞狀態,執行緒2獲取cpu執行權,然後進行if判斷此時instance還是null,因此執行緒2為instance賦值建立了該單例物件,那麼等到執行緒1再次獲取cpu執行權,也進行了instance賦值建立了該單例物件,單例模式被破壞。

2.懶漢式-方式2 (執行緒安全)

我們可以通過加synchronized同步鎖的方式保證單例模式在多執行緒下依舊有效

 public static synchronized Demo3 getInstance(){
        //在首次使用該物件時建立,因此instance賦值也就是物件建立 就是在外界獲取該單例類的方法getInstance()中建立


        // 如果instance為null,表明還沒有建立該類的物件,那麼就進行建立

        if(instance == null){
          instance = new Demo3();
        }
        //如果instance不為null,表明已經建立過該類的物件,根據單例類只能建立一個物件的特點,因此我們直接返回instance
        return instance;
    }

注意:

雖然保證了執行緒安全問題,但是在getInstance()方法上新增了synchronized關鍵字,導致該方法執行效率很低(這是加鎖的一個常見問題)。其實我們可以很容易發現,我們只是在判斷instance時需要解決多執行緒的安全問題,而沒必要在getInstance()上加鎖

3.懶漢式-方式3(雙重檢查鎖)

對於getInstance()方法來說,絕大部分的操作都是讀操作,讀操作是執行緒安全的,沒必要讓每個執行緒必須持有鎖才能呼叫該方法,我們可以調整加鎖的時機。

public class Demo4 {
    /**
     *私有構造方法  讓外界不能建立該類物件
     */
    private Demo4(){}

    /**
     *
     * 沒有進行賦值(建立物件) 只是宣告了一個該類的變數
     */
    private static Demo4 instance;


    /**
     * 提供一個公共的存取方式,讓外界可以獲取該類的物件 static是因為外界不需要建立物件,直接通過類存取
     */
    public static  Demo4 getInstance(){


        // (第一次判斷)如果instance為null,表明還沒有建立該類的物件,那麼就進行建立
        if(instance == null){
            synchronized (Demo4.class){
                //第二次判斷 如果instance不為null
                if(instance == null){
                    instance = new Demo4();
                }
            }

        }

        //如果instance不為null,表明已經建立過該單例類的物件,不需要搶佔鎖,直接返回
        return instance;
    }

}

雙重檢查鎖模式完美的解決了單例、效能、執行緒安全問題,但是隻是這樣還是有問題的…

JVM在建立物件時會進行優化和指令重排,在多執行緒下可能會發生空指標異常的問題,可以使用volatile關鍵字,volatile可以保證可見性和有序性。

 private static volatile Demo4  instance;

如果發生指令重排 2 和 3 的步驟顛倒,那麼instance會指向一塊虛無的記憶體(也有可能是有資料的一塊記憶體)

完整程式碼

public class Demo4 {
    /**
     *私有構造方法  讓外界不能建立該類物件
     */
    private Demo4(){}

    /**
     * volatile可以保證有序性
     * 沒有進行賦值(建立物件) 只是宣告了一個該類的變數
     */
    private static volatile Demo4  instance;
    
    /**
     * 提供一個公共的存取方式,讓外界可以獲取該類的物件 static是因為外界不需要建立物件,直接通過類存取
     */
    public static  Demo4 getInstance(){
        // (第一次判斷)如果instance為null,表明還沒有建立該類的物件,那麼就進行建立
        if(instance == null){
            synchronized (Demo4.class){
                //第二次判斷 如果instance不為null
                if(instance == null){
                    instance = new Demo4();
                }
            }
        }

        //如果instance不為null,表明已經建立過該單例類的物件,不需要搶佔鎖,直接返回
        return instance;
    }
}

4.懶漢式-4 (靜態內部類)

靜態內部類單例模式中範例由內部類建立,由於JVM在載入外部類的過程中,是不會載入靜態內部類的,只有內部類的屬性/方法被呼叫時才會被載入,並初始化其靜態屬性。靜態屬性由於被final修飾,保證只被範例化一次,並且嚴格保證範例化順序。

建立單例類

public class Singleton {

    private Singleton(){}

    /**
     *定義一個靜態內部類
     */
    private static  class SingletonHolder{
        //在靜態內部類中建立外部類的物件
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

建立測試類

public class Test4 {
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();

        Singleton instance1 = Singleton.getInstance();
        //判斷兩個物件是否是同一個
        System.out.println(instance == instance1);
    }
}

注意:

​ 第一次載入Singleton類時不會去初始化INSTANCE,只有在呼叫getInstance()方法時,JVM載入SingletonHolder並初始化INSTANCE,這樣可以保證執行緒安全,並且Singleton類的唯一性

​ 靜態內部類單例模式是一種開源專案比較常用的單例模式,在沒有任何加鎖的情況下保證多執行緒的安全,並且沒有任何效能和空間上的浪費

3.單例模式的破壞

單例模式最重要的一個特點就是隻能建立一個範例物件,那麼如果能使單例類能建立多個就破壞了單例模式(除了列舉方式)破壞單例模式的方式有兩種:

3.1序列化和反序列化

從以上建立單例模式的方式中任選一種(除列舉方式),例如靜態內部類方式

//記得要實現Serializable序列化介面
public class Singleton implements Serializable {

    private Singleton(){}

    /**
     *定義一個靜態內部類
     */
    private static  class SingletonHolder{
        //在靜態內部類中建立外部類的物件
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

測試類

public class Test1 {

    public static void main(String[] args) throws IOException {
              writeObjectToFile();
    }


    /**
     * 向檔案中寫資料(物件)
     * @throws IOException
     */
    public static void writeObjectToFile() throws IOException {
        //1.獲取singleton物件
        Singleton instance = Singleton.getInstance();
        //2.建立物件輸出流物件
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:\1.txt"));
        //3.寫物件
        oos.writeObject(instance);
        //4.釋放資源
        oos.close();


    }
}

在D槽根目錄下出現一個檔案1.txt由於資料是序列化後的 咱也看不懂

然後我們從這個檔案中讀取instance物件

public static void main(String[] args) throws Exception {
             // writeObjectToFile();
        readObjectFromFile();
        readObjectFromFile();
    }
    /**
     * 從檔案中讀資料(物件)
     * @throws Exception
     */
    public static  void readObjectFromFile() throws Exception {

        //1.建立物件輸入流物件
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:\1.txt"));
        //2.讀物件
        Singleton instance = (Singleton) ois.readObject();
        System.out.println(instance);
        //3.釋放資源
        ois.close();
    }

輸出結果不相同,結論為:序列化破壞了單例模式,兩次讀的物件不一樣了

com.xue.demo01.Singleton@2328c243
com.xue.demo01.Singleton@bebdb06

解決方案

在singleton中新增readResolve方法

  /**
     * 當進行反序列化時,會自動呼叫該方法,將該方法的返回值直接返回
     * @return
     */
    public Object readResolve(){
        return SingletonHolder.INSTANCE;
    }

重新進行寫和讀,發現兩次讀的結果是相同的,解決了序列化破壞單例模式的問題

為什麼在singleton單例類中新增readResolve方法就可以解決序列化破壞單例的問題呢,我們在ObjectInputStream原始碼中在readOrdinaryObject方法中

 private Object readOrdinaryObject(boolean unshared)
        throws IOException{
//程式碼段   
Object obj;
        try {
            //isInstantiable如果一個實現序列化的類在執行時被範例化就返回true
            //desc.newInstance()會通過反射呼叫無參構造建立一個新的物件
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

   //程式碼段

   if (obj != null &&
            handles.lookupException(passHandle) == null &&
       //hasReadResolveMethod 如果實現序列化介面的類中定義了readResolve方法就返回true
            desc.hasReadResolveMethod())
        {
       //通過反射的方式呼叫被反序列化類的readResolve方法
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
       
    //程式碼段
 }

3.2反射

從以上建立單例模式的方式中任選一種(除列舉方式),例如靜態內部類方式

測試類

public class Test1 {

    public static void main(String[] args) throws Exception {


        //1.獲取Singleton的位元組碼物件
        Class<Singleton> singletonClass = Singleton.class;

        //2.獲取無參構造方法物件
        Constructor cons = singletonClass.getDeclaredConstructor();

        //3.取消存取檢查
        cons.setAccessible(true);
        //4.反射建立物件
        Singleton instance1 = (Singleton) cons.newInstance();

        Singleton instance2 = (Singleton) cons.newInstance();

        System.out.println(instance1 == instance2);
        //輸出false 說明反射破壞了單例模式
    }


}

解決方案:

public class Singleton  {

    //static是為了都能存取
    private static boolean flag = false;

    private Singleton() {
        //加上同步鎖,防止多執行緒並行問題
        synchronized (Singleton.class) {
            //判斷flag是否為true,如果為true說明不是第一次建立,拋異常
            if (flag) {
                throw new RuntimeException("不能建立多個物件");
            }
            //flag的值置為true
            flag = true;
        }
    }

    /**
     *定義一個靜態內部類
     */
    private static  class SingletonHolder{
        //在靜態內部類中建立外部類的物件
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

這樣就不能通過之前的反射方式破壞單例模式了,但是如果通過反射修改flag的值也是可以破壞單例模式的,但是這樣可以防止意外反射破壞單例模式,如果刻意破壞是很難防範的,畢竟反射太強了


IT145.com E-mail:sddin#qq.com