設計模式開門之單例模式

有点儿意思發表於2024-12-05

單例模式歸屬於設計模式大類(建立型、結構型、行為型)設計模式中的建立型模式。

一個類只允許建立一個物件(例項)那麼這個類就是單例類,這個設計模式就是單例設計模式。單例設計模式的這個類提供了一種訪問其唯一物件的方式,可以直接訪問,不需要例項化該物件,提供了一個全域性訪問點來訪問該例項。

本文從(1)為什麼使用單例?(2)如何實現一個單例模式?(3)單例模式可能存在哪些問題?三個方面簡單聊聊

為什麼使用單例?

處理資源訪問衝突

FileWriter本身就是執行緒安全的,它的內部實現中本身就加了物件級別的鎖,因此,在外層呼叫write()函式的時候,再加物件級別的鎖實際上是多此一舉。因為不同的Logger物件不共享FileWriter物件,所以,FileWriter物件級別的鎖也解決不了資料寫入互相覆蓋的問題。

那我們該怎麼解決這個問題呢?實際上,要想解決這個問題也不難,我們只需要把物件級別的鎖,換成類級別的鎖就可以了讓所有的物件都共享同一把鎖。這樣就避免了不同物件之間同時呼叫log()函式,而導致的日誌覆蓋問題。

public class Logger {
  private FileWriter writer;
  private static final Logger instance = new Logger();  // 只建立一個例項instance

  private Logger() {
    File file = new File("/xx/xx/xx.txt");
    writer = new FileWriter(file, true); //true表示追加寫入
  }
  
  public static Logger getInstance() {
    return instance;
  }
  
  public void log(String message) {
    writer.write(mesasge);
  }
}

// Logger類的應用示例:
public class UserController {
  public void login(String username, String password) {
    // 業務程式碼
    Logger.getInstance().log(username + "xx");
  }
}

public class OrderController {  
  public void create(OrderVo order) {
    // 業務程式碼
    Logger.getInstance().log("xx" + order.toString());
  }
}

表示全域性唯一類

例如唯一遞增號碼生成器、呼叫鏈路traceId追蹤,如果程式中存在兩個重複的物件那麼就可能造成重複的ID出現,所以應將ID的生成設計成單例的。

import java.util.concurrent.atomic.AtomicLong;
public class IdGenerator {
  // AtomicLong是一個Java併發庫中提供的一個原子變數型別,它將一些執行緒不安全需要加鎖的複合操作封裝為了執行緒安全的原子操作,
  // 比如下面會用到的incrementAndGet().
  private AtomicLong id = new AtomicLong(0);
  private static final IdGenerator instance = new IdGenerator();
  private IdGenerator() {}
  public static IdGenerator getInstance() {
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}
// IdGenerator使用舉例
long id = IdGenerator.getInstance().getId();

如何實現一個單例?

餓漢式

在類載入的時候,instance靜態例項就已經建立並初始化好了,所以,instance例項的建立過程是執行緒安全的。不過,這樣的實現方式不支援延遲載入(在真正用到IdGenerator的時候,再建立例項)'就是很飢餓,類載入完成的時候 例項也跟著初始化完成了'

public class IdGenerator {
  private AtomicLong id = new AtomicLong(0);
  private static final IdGenerator instance = new IdGenerator(); // 建立單個例項
  private IdGenerator() {}
  public static IdGenerator getInstance() {   //getInstance返回例項
    return instance;
  }
  public long getId() {   // 方法呼叫
    return id.incrementAndGet();
  }
}

懶漢式

相對於餓漢式來說,懶漢式就是什麼時候使用,什麼時候再去載入。支援延遲載入

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private IdGenerator() {}
  // 類級別的鎖
  public static synchronized IdGenerator getInstance() {
    if (instance == null) {
       // 初始沒有 需要再去載入
      instance = new IdGenerator();
    }
    return instance;
  }
  public long getId() {
    return id.incrementAndGet();
  }
}

以上懶漢式在初始化例項的時候加了一把鎖,導致函式的併發度很低,如果是簡單呼叫還可以接受

餓漢式與懶漢式的簡單比較:

關於這倆的優缺點,也是仁者見仁智者見智。餓漢式有人認為因為不支援延遲載入,如果例項佔用資源多(比如佔用記憶體多)或初始化耗時長(比如需要載入各種配置檔案),提前初始化例項是一種浪費資源的行為。最好的方法應該在用到的時候再去初始化。

也有人對餓漢式持支援意見認為有問題就要及早暴露。如果需要很多資源,那麼等到需要的時候再去載入,比較耗時的載入過程也可能會影響系統的效能(比如,在響應客戶端介面請求的時候,做這個初始化操作,會導致此請求的響應時間變長,甚至超時)以下這句是借鑑江湖道友的:

"如果例項佔用資源多,按照fail-fast的設計原則(有問題及早暴露),那我們也希望在程式啟動時就將這個例項初始化好。如果資源不夠,就會在程式啟動的時候觸發報錯(比如Java中的 PermGen Space OOM),我們可以立即去修復。這樣也能避免在程式執行一段時間後,突然因為初始化這個例項佔用資源過多,導致系統崩潰,影響系統的可用性。"

雙重檢測

既支援延遲檢測也支援高併發

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private IdGenerator() {}
  public static IdGenerator getInstance() {
    if (instance == null) {
      synchronized(IdGenerator.class) { // 此處為類級別的鎖
        if (instance == null) {
          instance = new IdGenerator();
        }
      }
    }
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

靜態內部類

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private IdGenerator() {}

  private static class SingletonHolder{
    private static final IdGenerator instance = new IdGenerator();
  }
  
  public static IdGenerator getInstance() {
    return SingletonHolder.instance;
  }
    
  public long getId() { 
    return id.incrementAndGet();
  }
}

當載入IdGenerator時內部類SingletonHolder不會立即建立例項物件,只有呼叫getInstance()方法時,SingletonHolder才會被載入,這個時候才會建立instance物件。instance的唯一性、建立過程的執行緒安全性,都由JVM來保證。所以,這種實現方法既保證了執行緒安全,又能做到延遲載入。

存在的問題?

對OOP的特性支援不太友好

針對生成唯一的ID自增生成器來說,如果我們想要對不同的業務採用不同的ID生成演算法,比如訂單類和使用者類採用不同的演算法,那我們就要修改所有用到生成唯一自增ID的地方,改動較大。可能就需要對不同業務給出同步的ID生成器

會隱藏類之間的依賴關係

由於單例類不需要顯式建立,不需要依賴引數傳遞,在程式碼中直接呼叫即可。如果程式碼比較複雜,呼叫關係比較隱蔽就比較難以找出這個類依賴了哪些單例類。

......

以上內容有參考成分,歡迎指導學習,不喜勿噴~

相關文章