博客 / 詳情

返回

一、單例模式

一、模式定義:

  • 保證一個類只有一個實例,並且提供一個全局訪問點

二、使用場景:

  • 重量級對象,不需要多個實例,如:線程池,數據庫連接池

三、類圖:

clipboard

四、不同方式創建的單例設計與區別

4.1、懶漢單例模式

  懶漢單例模式的特點是延遲加載,只有在真正使用的時候,才開始實例化,用該方式創建單例對象時,對於線程安全問題和指令重排序導致的初始化並且引用賦值失敗的問題,可以用以下2種方式來解決:
①、對於線程安全問題,可以使用【double check】加【鎖】進行優化;
②、對於編譯器(JIT),CPU可能對指令進行重排序,導致使用尚未初始化的單例實例,因此,需要通過對創建的單例對象添加volatile關鍵字進行修飾,來防止指令重排序。
懶漢單例模式實現單例的方式,如下所示:

class Service{
   public static LazySingleton instance = null;
   public static void main(String[] args) {
      new Thread(new Runnable() {
         public void run() {
            instance = LazySingleton.getInstance();
            System.out.println(instance);
         }
      }).start();
      new Thread(new Runnable() {
         public void run() {
            instance = LazySingleton.getInstance();
            System.out.println(instance);
         }
      }).start();
   }
}

/**
 * 懶漢式:當第一次去使用Singleton對象的時候才會為其產生實例化對象的操作
 * */
public class LazySingleton {
    //volatile關鍵字可以保證這個對象的賦值順序按照以下順序來執行:
    //1、分配空間,2、初始化,3、引用賦值
   private volatile static LazySingleton instance = null;
   
   private LazySingleton() {
   }
   /**
    * 外部調用來獲取單例對象的函數
    */
   public static LazySingleton getInstance() {
      if (instance == null) {
         synchronized (LazySingleton.class){
            if (instance == null){
               instance = new LazySingleton();
            }
         }
      }
      return instance;
   }
}

上面代碼的運行結果如下:
image

  • volatile關鍵字可以在字節碼層面保證這個對象的賦值順序按照以下順序來執行:1、分配空間,2、初始化,3、引用賦值。如果沒有volatile關鍵字,可能在起始初始化的時候,拿到的是空對象。
    clipboard
4.2、餓漢單例模式

  餓漢單例模式的特點是類加載的時候進行初始化,通過JVM的類加載機制來保證單例。餓漢單例模式實現單例的方式,如下所示:

public class HungrySingleton {
   /**
    * 在類的內部可以訪問私有結構,所以可以在類的內部產生實例化對象
    */
   private static HungrySingleton instance = new HungrySingleton();

   /**
    * private 聲明構造
    */
   private HungrySingleton() { }

   /**
    * 返回對象實例
    */
   public static HungrySingleton getInstance(){
      return instance;

   }
}
4.3、靜態內部類實現單例

  靜態內部類實現單例的特點是隻有真正使用對應的類時,才會觸發初始化,使用方式包括以下5種:
①、new操作;
②、訪問靜態屬性;
③、訪問靜態方法;
④、用反射訪問類;
⑤、初始化一個類的子類;
靜態內部類實現單例的方式,如下所示:

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class ServiceInnerClass{
   public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
      //通過反射方式創建InnerClassSingleton實例:
      Constructor<InnerClassSingleton> declaredConstructor = InnerClassSingleton.class.getDeclaredConstructor();
      declaredConstructor.setAccessible(true);
      //因為用private修飾的InnerClassSingleton的構造函數中進行了判斷,防止反射方式創建這個對象實例,因此:
      // ①、反射在加載InnerClassSingleton.class文件前,已經加載了InnerClassSingleton的靜態內部類。
      //②、加載靜態內部類時,也加載了靜態instance變量,進行初始化。
      //③、因此,反射方式在創建實例時,會拋出運行時異常。
      InnerClassSingleton innerClassSingleton = declaredConstructor.newInstance();
      
       //只能通過InnerClassSingleton.class的getInstance()函數來獲取這個單例
      InnerClassSingleton instance = InnerClassSingleton.getInstance();
   }
}

class InnerClassSingleton {

   //4、靜態內部類,在加載外部類的時候,該類和該類中的靜態變量被加載
   private static class InnerClassHolder{
      private static InnerClassSingleton instance = new InnerClassSingleton();

   }
   //1、提供private修飾的私有構造方法,保證外部的類無法對該類進行初始化
   private InnerClassSingleton(){
      //5、進行判斷,防止反射方式創建這個對象實例
      if(InnerClassHolder.instance != null){
         throw new RuntimeException("單例模式,不允許多個實例");
      }
   }
   //2、只有在加載這個類的時候,才能拿使用這個靜態函數。
   public static InnerClassSingleton getInstance(){
      //3、該靜態函數內同時加載靜態內部類,來初始化InnerClassSingleton對象
      return InnerClassHolder.instance;
   }
}
4.4、從序列化文件中讀取一個類的實例

從序列化文件中讀取一個類的實例實現單例的特點是該實例需要實現Serializable.interface接口,Serializable.interface接口的特點,如下所示:
clipboard
clipboard

從序列化文件中讀取一個類的實例來實現單例的方式,如下所示:

import java.io.*;
import java.lang.reflect.InvocationTargetException;

class ServiceInnerClass{
   public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException {
      InnerClassSingleton instance = InnerClassSingleton.getInstance();
      ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testSerializable"));
      oos.writeObject(instance);
      oos.close();
      //將剛才的對象實例序列化之後,再進行反序列化。得到的實例相同。
      ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testSerializable"));
      InnerClassSingleton innerClassSingleton = (InnerClassSingleton) ois.readObject();
      ois.close();


      System.out.println(innerClassSingleton == instance);



   }
}

public class InnerClassSingleton implements Serializable {
   //序列化版本號。不加的話,如果序列化之後,進行修改類中的內容,則不能進行反序列化。
   private static final long serialVersionUID = 42L;

   //4、靜態內部類,在加載外部類的時候,該類和該類中的靜態變量被加載
   private static class InnerClassHolder{
      private static InnerClassSingleton instance = new InnerClassSingleton();

   }
   //1、提供私有構造方法,保證外部的類無法對該類
   private InnerClassSingleton(){
      //5、進行判斷,防止反射方式創建這個對象實例
      if(InnerClassHolder.instance != null){
         throw new RuntimeException("單例模式,不允許多個實例");
      }
   }
   //2、只有在加載這個類的時候,才能拿使用靜態方法。
   public static InnerClassSingleton getInstance(){
      //3、同時加載靜態內部類,來初始化InnerClassSingleton對象
      return InnerClassHolder.instance;
   }

   private Object readResolve() throws ObjectStreamException {
      return InnerClassHolder.instance;
   }

}

上面代碼的運行結果如下:
image

五、單例模式的應用

  • JDK源碼中Runtime.class,使用了餓漢單例模式
    clipboard

  • spring框架中的ReactiveAdapterRegistry.class,使用了懶漢單例模式:
    clipboard
    clipboard

六、JDK源碼中,Serializable接口的應用

  • 貨幣類Currency類
    clipboard
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.