008.設計模式與範式:創建型--單例模式

一.單例模式(上):爲什麼說支持懶加載的雙重檢測不比餓漢式更優?

1. 爲什麼要使用單例?
  1. 單例設計模式

一個類只允許創建一個對象(或者實例),那這個類就是一個單例類,這種設計模式就叫作單例設計模式,簡稱單例模式

  1. 例子:向文件寫日誌----通過加鎖方式,給log函數加互斥鎖。

public class Logger {
  private FileWriter writer;

  public Logger() {
    File file = new File("/Users/wangzheng/log.txt");
    writer = new FileWriter(file, true); //true表示追加寫入
  }
  
  public void log(String message) {
    synchronized(this) {
      writer.write(mesasge);
    }
  }
}

問題分析:

這真的能解決多線程寫入日誌時互相覆蓋的問題嗎?答案是否定的。這是因爲,這種鎖是一個對象級別的鎖,一個對象在不同的線程下同時調用 log() 函數,會被強制要求順序執行。但是,不同的對象之間並不共享同一把鎖

  1. 改寫2:只需要把對象級別的鎖,換成類級別的鎖就可以了。讓所有的對象都共享同一把鎖。這樣就避免了不同對象之間同時調用 log() 函數

public class Logger {
  private FileWriter writer;

  public Logger() {
    File file = new File("/Users/wangzheng/log.txt");
    writer = new FileWriter(file, true); //true表示追加寫入
  }
  
  public void log(String message) {
    synchronized(Logger.class) { // 類級別的鎖
      writer.write(mesasge);
    }
  }
}
  1. 相對於這兩種解決方案,單例模式的解決思路就簡單一些了。單例模式相對於之前類級別鎖的好處是,不用創建那麼多 Logger 對象,一方面節省內存空間,另一方面節省系統文件句柄(對於操作系統來說,文件句柄也是一種資源,不能隨便浪費)

  2. 單例模式


public class Logger {
  private FileWriter writer;
  private static final Logger instance = new Logger();

  private Logger() {
    File file = new File("/Users/wangzheng/log.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 + " logined!");
  }
}

public class OrderController {  
  public void create(OrderVo order) {
    // ...省略業務邏輯代碼...
    Logger.getInstance().log("Created a order: " + order.toString());
  }
}
2. 如何實現一個單例?
  1. 關注的點
  1. 構造函數需要是 private 訪問權限的,這樣才能避免外部通過 new 創建實例;
  2. 考慮對象創建時的線程安全問題;
  3. 考慮是否支持延遲加載;
  4. 考慮 getInstance() 性能是否高(是否加鎖)。
3. 餓漢式
  1. 餓漢式的實現方式比較簡單。在類加載的時候,instance 靜態實例就已經創建並初始化好了,所以,instance 實例的創建過程是線程安全的。不過,這樣的實現方式不支持延遲加載(在真正用到 IdGenerator 的時候,再創建實例)

  2. 代碼實現


public class IdGenerator { 
  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();
  }
}
  1. 採用餓漢式實現方式,將耗時的初始化操作,提前到程序啓動的時候完成,這樣就能避免在程序運行的時候,再去初始化導致的性能問題。
4. 懶漢式
  1. 懶漢式相對於餓漢式的優勢是支持延遲加載
  2. 代碼實現:

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();
  }
}
  1. 問題分析

懶漢式的缺點也很明顯,我們給 getInstance() 這個方法加了一把大鎖(synchronzed),導致這個函數的併發度很低。量化一下的話,併發度是 1,也就相當於串行操作了。而這個函數是在單例使用期間,一直會被調用。如果這個單例類偶爾會被用到,那這種實現方式還可以接受。但是,如果頻繁地用到,那頻繁加鎖、釋放鎖及併發度低等問題,會導致性能瓶頸,這種實現方式就不可取了

5. 雙重檢測
  1. 餓漢式不支持延遲加載,懶漢式有性能問題,不支持高併發。那我們再來看一種既支持延遲加載、又支持高併發的單例實現方式,也就是雙重檢測實現方式。
  2. 在這種實現方式中,只要 instance 被創建之後,即便再調用 getInstance() 函數也不會再進入到加鎖邏輯中了。所以,這種實現方式解決了懶漢式併發度低的問題

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();
  }
}
6. 靜態內部類
  1. 一種比雙重檢測更加簡單的實現方法,那就是利用 Java 的靜態內部類。它有點類似餓漢式,但又能做到了延遲加載
  2. 代碼實現

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();
  }
}
  1. SingletonHolder 是一個靜態內部類,當外部類 IdGenerator 被加載的時候,並不會創建 SingletonHolder 實例對象。只有當調用 getInstance() 方法時,SingletonHolder 纔會被加載,這個時候纔會創建 instance。instance 的唯一性、創建過程的線程安全性,都由 JVM 來保證。所以,這種實現方法既保證了線程安全,又能做到延遲加載。
7. 枚舉
  1. 一種最簡單的實現方式,基於枚舉類型的單例實現。這種實現方式通過 Java 枚舉類型本身的特性,保證了實例創建的線程安全性和實例的唯一性
  2. 代碼實現

public enum IdGenerator {
  INSTANCE;
  private AtomicLong id = new AtomicLong(0);
 
  public long getId() { 
    return id.incrementAndGet();
  }
}

二.單例模式(中):爲什麼不推薦使用單例模式?又有何替代方案?

1. 單例存在哪些問題?
1. 單例對 OOP 特性的支持不友好
  1. 生成ID例子代碼:

public class Order {
  public void create(...) {
    //...
    long id = IdGenerator.getInstance().getId();
    //...
  }
}

public class User {
  public void create(...) {
    // ...
    long id = IdGenerator.getInstance().getId();
    //...
  }
}
  1. 問題:IdGenerator 的使用方式違背了基於接口而非實現的設計原則,也就違背了廣義上理解的 OOP 的抽象特性。如果未來某一天,我們希望針對不同的業務採用不同的 ID 生成算法。比如,訂單 ID 和用戶 ID 採用不同的 ID 生成器來生成。爲了應對這個需求變化,我們需要修改所有用到 IdGenerator 類的地方,這樣代碼的改動就會比較大。
  2. 改動後:

public class Order {
  public void create(...) {
    //...
    long id = IdGenerator.getInstance().getId();
    // 需要將上面一行代碼,替換爲下面一行代碼
    long id = OrderIdGenerator.getIntance().getId();
    //...
  }
}

public class User {
  public void create(...) {
    // ...
    long id = IdGenerator.getInstance().getId();
    // 需要將上面一行代碼,替換爲下面一行代碼
    long id = UserIdGenerator.getIntance().getId();
  }
}
2. 單例會隱藏類之間的依賴關係
  1. 單例類不需要顯示創建、不需要依賴參數傳遞,在函數中直接調用就可以了。如果代碼比較複雜,這種調用關係就會非常隱蔽
3. 單例對代碼的擴展性不友好
  1. 慢 SQL 與其他 SQL 隔離開來執行。爲了實現這樣的目的,我們可以在系統中創建兩個數據庫連接池,慢 SQL 獨享一個數據庫連接池,其他 SQL 獨享另外一個數據庫連接池,這樣就能避免慢 SQL 影響到其他 SQL 的執行
4. 單例對代碼的可測試性不友好
5. 單例不支持有參數的構造函數
  1. 比如我們創建一個連接池的單例對象,我們沒法通過參數來指定連接池的大小。
  2. 可使用其他方法
6. 單例不支持有參數的構造函數
2. 有何替代解決方案?
  1. 我們還可以用靜態方法來實現。不過,靜態方法這種實現思路,並不能解決我們之前提到的問題。如果要完全解決這些問題,我們可能要從根上,尋找其他方式來實現全局唯一類了。比如,通過工廠模式、IOC 容器(比如 Spring IOC 容器)來保證,由程序員自己來保證(自己在編寫代碼的時候自己保證不要創建兩個類對象)。

三.單例模式(下):如何設計實現一個集羣環境下的分佈式單例模式?

1. 如何理解單例模式中的唯一性?
  1. 單例定義

一個類只允許創建唯一一個對象(或者實例),那這個類就是一個單例類,這種設計模式就叫作單例設計模式,簡稱單例模式。

  1. 唯一性理解

單例模式創建的對象是進程唯一的,單例類在老進程中存在且只能存在一個對象,在新進程中也會存在且只能存在一個對象。而且,這兩個對象並不是同一個對象,這也就說,單例類中對象的唯一性的作用範圍是進程內的,在進程間是不唯一的

2. 如何實現線程唯一的單例?
  1. 線程唯一 && 進程唯一

“進程唯一”指的是進程內唯一,進程間不唯一。類比一下,“線程唯一”指的是線程內唯一,線程間可以不唯一。實際上,“進程唯一”還代表了線程內、線程間都唯一,這也是“進程唯一”和“線程唯一”的區別之處。這段話聽起來有點像繞口令,我舉個例子來解釋一下。

  1. 不同的線程對應不同的對象,同一個線程只能對應一個對象。實際上,Java 語言本身提供了 ThreadLocal 工具類,可以更加輕鬆地實現線程唯一單例。不過,ThreadLocal 底層實現原理也是基於下面代碼中所示的 HashMap。
  2. 實現

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

  private static final ConcurrentHashMap<Long, IdGenerator> instances
          = new ConcurrentHashMap<>();

  private IdGenerator() {}

  public static IdGenerator getInstance() {
    Long currentThreadId = Thread.currentThread().getId();
    instances.putIfAbsent(currentThreadId, new IdGenerator());
    return instances.get(currentThreadId);
  }

  public long getId() {
    return id.incrementAndGet();
  }
}
3. 如何實現集羣環境下的單例?
  1. 集羣唯一

“進程唯一”指的是進程內唯一、進程間不唯一。“線程唯一”指的是線程內唯一、線程間不唯一。集羣相當於多個進程構成的一個集合,“集羣唯一”就相當於是進程內唯一、進程間也唯一。也就是說,不同的進程間共享同一個對象,不能創建同一個類的多個對象。

  1. 實現:需要把這個單例對象序列化並存儲到外部共享存儲區(比如文件)。進程在使用這個單例對象的時候,需要先從外部共享存儲區中將它讀取到內存,並反序列化成對象,然後再使用,使用完成之後還需要再存儲回外部共享存儲區。
  2. 爲了保證任何時刻,在進程間都只有一份對象存在,一個進程在獲取到對象之後,需要對對象加鎖,避免其他進程再將其獲取。在進程使用完這個對象之後,還需要顯式地將對象從內存中刪除,並且釋放對對象的加鎖。
  3. 實現代碼

public class IdGenerator {
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private static SharedObjectStorage storage = FileSharedObjectStorage(/*入參省略,比如文件地址*/);
  private static DistributedLock lock = new DistributedLock();
  
  private IdGenerator() {}

  public synchronized static IdGenerator getInstance() 
    if (instance == null) {
      lock.lock();
      instance = storage.load(IdGenerator.class);
    }
    return instance;
  }
  
  public synchroinzed void freeInstance() {
    storage.save(this, IdGeneator.class);
    instance = null; //釋放對象
    lock.unlock();
  }
  
  public long getId() { 
    return id.incrementAndGet();
  }
}

// IdGenerator使用舉例
IdGenerator idGeneator = IdGenerator.getInstance();
long id = idGenerator.getId();
IdGenerator.freeInstance();
4. 如何實現一個多例模式?
  1. 多例模式:“單例”指的是,一個類只能創建一個對象。對應地,“多例”指的就是,一個類可以創建多個對象,但是個數是有限制的,比如只能創建 3 個對象
  2. 實現:

public class BackendServer {
  private long serverNo;
  private String serverAddress;

  private static final int SERVER_COUNT = 3;
  private static final Map<Long, BackendServer> serverInstances = new HashMap<>();

  static {
    serverInstances.put(1L, new BackendServer(1L, "192.134.22.138:8080"));
    serverInstances.put(2L, new BackendServer(2L, "192.134.22.139:8080"));
    serverInstances.put(3L, new BackendServer(3L, "192.134.22.140:8080"));
  }

  private BackendServer(long serverNo, String serverAddress) {
    this.serverNo = serverNo;
    this.serverAddress = serverAddress;
  }

  public BackendServer getInstance(long serverNo) {
    return serverInstances.get(serverNo);
  }

  public BackendServer getRandomInstance() {
    Random r = new Random();
    int no = r.nextInt(SERVER_COUNT)+1;
    return serverInstances.get(no);
  }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章