Java常用設計模式-單例模式

柳~發表於2024-07-23

Java常用設計模式-單例模式

Java Design Patterns:

建立型模式:工廠方法、抽象方法、建造者、原型、單例

結構型模式有:介面卡、橋接、組合、裝飾器、外觀、享元、代理

行為型模式有:責任鏈、命令、直譯器、迭代器、中介、備忘錄、觀察者、狀態、策略、模板方法、訪問者

常用設計模式:

單例模式、工廠模式、代理模式、策略模式&模板模式、門面模式、責任鏈模式、裝飾器模式、組合模式、builder模式

單例模式

簡介

確保一個類只有一個例項,並提供一個全域性訪問點

懶漢式:

/**
 * 單例設計模式:確保一個類只有一個物件例項,並提供一個全域性訪問點
 * 懶漢式:
 * 是否 Lazy 初始化:是
 * 是否多執行緒安全:否
 */
public class Signleton {
    private static Signleton signleton;
    private Signleton(){}
    //可以透過synchronized關鍵字保證執行緒安全
    public static Signleton getSignleton(){
        if(signleton == null){
            signleton = new Signleton();
        }
        return signleton;
    }
}

餓漢式:

/**
 * 單例設計模式:確保一個類只有一個物件例項,並提供一個全域性訪問點
 * 餓漢式:
 * 是否 Lazy 初始化:否
 * 是否多執行緒安全:是
 */
class Signleton1{
    private static Signleton1 signleton1 = new Signleton1();

    private Signleton1(){}

    public static Signleton1 getSignleton1(){
        return signleton1;
    }
}

懶漢式:解決反射、序列化反序列化問題

/**
 * 單例設計模式:確保一個類只有一個物件例項,並提供一個全域性訪問點
 * 懶漢式:
 * 是否 Lazy 初始化:是
 * 是否多執行緒安全:否
 */
public class Signleton implements Serializable {
    private static final long serialVersionUID = 1L;
    private static Signleton signleton;

    private Signleton() {
        // 防止反射
        if (signleton != null) {
            throw new RuntimeException();
        }
    }

    // 可以透過synchronized關鍵字保證執行緒安全
    public static Signleton getSignleton() {
        if (signleton == null) {
            signleton = new Signleton();
        }
        return signleton;
    }


    /*
    序列化:當一個物件被序列化時,Java 將該物件的狀態寫入一個位元組流。
    反序列化:當位元組流被反序列化時,Java 將建立一個新的物件例項,並將位元組流中的資料填充到這個新例項中。
    readResolve 方法:在物件被反序列化之後,Java 會呼叫這個方法。如果該方法存在,返回的物件將代替預設反序列化過程中建立的新物件。
     */
    private Object readResolve() {
        return signleton;
    }

}


 /**
     * 反射測試
     */
    @Test
    public void test(){
        //獲取單例
        Signleton signleton = Signleton.getSignleton();
        Signleton signleton1 = Signleton.getSignleton();
        System.out.println(signleton.hashCode());
        System.out.println(signleton1.hashCode());

        //透過反射破壞單例
        try {
            Class<?> aClass = Class.forName("design.patterns.Signleton");
            Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
            declaredConstructor.setAccessible(true);
            Signleton signleton2 = (Signleton) declaredConstructor.newInstance();
            System.out.println(signleton2.hashCode());
        } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException |
                 InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    /**
     * 序列化測試
     */
    @Test
    public void test1(){
        //獲取單例
        Signleton signleton = Signleton.getSignleton();
        Signleton signleton1 = Signleton.getSignleton();
        System.out.println(signleton.hashCode());
        System.out.println(signleton1.hashCode());

        //序列化反序列化獲取物件
        try {
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("D:/signleton.ser"));
            outputStream.writeObject(signleton1);
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("D:/signleton.ser"));
            Signleton signleton2 = (Signleton) inputStream.readObject();
            System.out.println(signleton2.hashCode());
        } catch (IOException | ClassNotFoundException e) {
            // throw new RuntimeException(e);
            e.printStackTrace();
        }
    }

懶漢式DCL(推薦):雙重檢查鎖定(Double-Checked Locking)是用於減少同步開銷,同時保證執行緒安全的一種最佳化方法。其核心思想是:在訪問共享資源時,先進行一次非同步的檢查,如果未初始化,再進入同步塊進行第二次檢查和初始化。這樣可以避免每次呼叫獲取例項方法時都需要進行同步,從而提升效能。

這裡也確保序列化安全。

/**
 * 單例設計模式:確保一個類只有一個物件例項,並提供一個全域性訪問點
 * 懶漢式:
 * 是否 Lazy 初始化:是
 * 是否多執行緒安全:否
 */
public class Signleton implements Serializable {
  	private static final long serialVersionUID = 1L;
    private static volatile Signleton signleton;

    private Signleton() {}

    public static Signleton getSignleton() {
        if (signleton == null) {
        	synchronized(Signleton.class){
        		if(signleton == null){
        		 signleton = new Signleton();
        		}
        	}
           
        }
        return signleton;
    }
    
    /*
    序列化:當一個物件被序列化時,Java 將該物件的狀態寫入一個位元組流。
    反序列化:當位元組流被反序列化時,Java 將建立一個新的物件例項,並將位元組流中的資料填充到這個新例項中。
    readResolve 方法:在物件被反序列化之後,Java 會呼叫這個方法。如果該方法存在,返回的物件將代替預設反序列化過程中建立的新物件。
     */
    private Object readResolve() {
        return signleton;
    }
}

場景

資源共享:避免頻繁的建立銷燬某個物件,造成存好。比如:日誌檔案。

控制資源:避免過多的物件產生,造成其他問題。比如網站的計數器。

應用場景:

  • 日誌管理器:避免頻繁建立和銷燬日誌物件,確保日誌檔案只被一個例項操作,以便內容可以正確追加。

  • 網站計數器:全域性唯一例項用於統計網站訪問次數,避免併發更新問題。

  • Windows 回收站:整個系統執行過程中,回收站一直維護著唯一的一個例項。

  • 多執行緒的執行緒池:執行緒池需要方便控制池中的執行緒,單例模式確保執行緒池全域性唯一。

    • /**
       * 單例執行緒池 -- 應用場景
       */
      public class ThreadPool1 {
          private static ThreadPool1 threadPool;
          // 定義介面
          private ExecutorService executorService;
      
          private ThreadPool1() {
              executorService = new ThreadPoolExecutor(
                      5, // 核心執行緒數
                      10, // 匯流排程數
                      60, TimeUnit.MILLISECONDS, // 存活時間和單位
                      new LinkedBlockingDeque<Runnable>(),  // 用於儲存等待執行的任務的佇列
                      new ThreadFactory() {  // 用於建立新執行緒的工廠
                          // 定義原子操作的 int 型別。它可以在多執行緒環境下安全地進行自增、自減等操作而不需要同步
                          private AtomicInteger threadNumber = new AtomicInteger(1);
                          @Override
                          public Thread newThread(Runnable r) {
                              Thread thread = new Thread(r, "CustomThreadPool-thread-" + threadNumber.getAndIncrement());
                              // thread.setDaemon(true); // 設定為守護執行緒
                              thread.setPriority(Thread.NORM_PRIORITY);
                              return thread;
                          }
                      },
                      new ThreadPoolExecutor.CallerRunsPolicy() // 拒絕策略,當任務佇列滿了且無法再接受任務時的處理策略
              );
      
          }
      
          public static synchronized ThreadPool1 getThreadPool() {
              if (threadPool == null) {
                  threadPool = new ThreadPool1();
              }
              return threadPool;
          }
      
          public void submitTask(Runnable runnable) {
              executorService.submit(runnable);
          }
      
          public void shutdown() {
              executorService.shutdown();
          }
      }
      
      
      
          /**
           * 單例執行緒池測試
           */
          @Test
          public void test2(){
              Runnable runnable = () -> {
                  System.out.println(Thread.currentThread().getName() + " task is run");
              };
      
              // ThreadPool1.getThreadPool().submitTask(runnable);
      
              ThreadPool1 threadPool = ThreadPool1.getThreadPool();
              threadPool.submitTask(runnable);
              System.out.println(threadPool.hashCode());
      
              ThreadPool1 threadPool1 = ThreadPool1.getThreadPool();
              threadPool1.submitTask(runnable);
              System.out.println(threadPool1.hashCode());
          }
      
      //輸出:
      158199555
      158199555
      CustomThreadPool-thread-2 task is run
      CustomThreadPool-thread-1 task is run
      
  • SpringBoot中的大多數容器管理的Bean都是單例的,這些bean是應用程式級別的單例,也就是說不同使用者共享同一個例項。比如@RestController、@Service、@Compoment、@Configuration註解修飾的類,預設都是單例。

優點

控制資源的使用

  • 例項控制:確保一個類只有一個例項,避免了多個例項導致的資源浪費。例如,在資料庫連線池或執行緒池的設計中,單例模式確保只建立一個連線池或執行緒池例項,從而控制資源的使用。
  • 節省資源:減少了系統的開銷,避免了重複建立和銷燬物件的高昂成本。

全域性訪問點

  • 統一管理:透過提供一個全域性訪問點,可以方便地管理和訪問例項。比如,在日誌記錄系統中,透過單例模式可以確保所有日誌記錄都透過同一個例項進行處理,從而統一日誌的輸出格式和內容。
  • 一致性:所有對該例項的操作都透過統一的介面進行,確保了資料的一致性和完整性。

易於擴充套件

  • 延遲例項化:懶漢式單例模式在首次使用時才建立例項,避免了不必要的資源浪費。這種延遲載入的特性也便於在系統啟動時減少初始化時間。

缺點

潛在的資源爭用

  • 資源競爭:如果單例類內部使用了共享資源,而這些資源在高併發場景下沒有妥善處理,可能導致資源競爭問題。例如,某個單例例項持有資料庫連線物件,在高併發請求下可能導致連線池枯竭。

單點故障

  • 故障影響範圍大:如果單例例項出現問題,整個系統的相關功能可能會受到影響。例如,日誌記錄系統的單例例項出現異常,可能導致整個系統無法正常記錄日誌。

難以測試

  • 測試複雜性:由於單例模式在整個應用程式中只有一個例項,單元測試時可能會導致測試的隔離性和獨立性變差。例如,一個單例類的狀態在多個測試方法之間共享,可能導致測試結果互相影響。

隱藏依賴關係

  • 依賴性不明確:單例模式透過全域性訪問點訪問例項,可能會導致類與類之間的依賴關係變得不清晰。例如,一個類可能隱式依賴於某個單例類的狀態,增加了系統的複雜性和維護成本。

相關文章