[Java併發-18-併發設計模式] COW模式:Copy-on-Write模式的應用領域

在上一篇文章中咱們講到 Java 裏 String 這個類在實現 replace() 方法的時候,並無更改原字符串裏面 value[] 數組的內容,而是建立了一個新字符串,這種方法在解決不可變對象的修改問題時常常用到。若是你深刻地思考這個方法,你會發現它本質上是一種Copy-on-Write 方法。所謂 Copy-on-Write,常常被縮寫爲 COW 或者 CoW,顧名思義就是寫時複製數組

不可變對象的寫操做每每都是使用 Copy-on-Write 方法解決的,固然 Copy-on-Write 的應用領域並不侷限於 Immutability 模式。下面咱們先簡單介紹一下 Copy-on-Write 的應用領域,讓你對它有個更全面的認識。數據結構

Copy-on-Write 模式的應用領域

咱們知道 CopyOnWriteArrayList 和 CopyOnWriteArraySet 這兩個 Copy-on-Write 容器,它們背後的設計思想就是 Copy-on-Write;經過 Copy-on-Write 這兩個容器實現的讀操做是無鎖的,因爲無鎖,因此將讀操做的性能發揮到了極致。併發

除了上面咱們說的 Java 領域,不少其餘領域也都能看到 Copy-on-Write 的身影:Docker 容器鏡像的設計是 Copy-on-Write,甚至分佈式源碼管理系統 Git 背後的設計思想都有 Copy-on-Write。負載均衡

CopyOnWriteArrayList 和 CopyOnWriteArraySet 這兩個 Copy-on-Write 容器在修改的時候會複製整個數組,因此若是容器常常被修改或者這個數組自己就很是大的時候,是不建議使用的。反之,若是是修改很是少、數組數量也不大,而且對讀性能要求苛刻的場景,使用 Copy-on-Write 容器效果就很是好了。框架

一個真實案例

RPC 框架中有個基本的核心功能就是負載均衡。服務提供方是多實例分佈式部署的,因此服務的客戶端在調用 RPC 的時候,會選定一個服務實例來調用,這個選定的過程本質上就是在作負載均衡,而作負載均衡的前提是客戶端要有所有的路由信息。分佈式

例如在下圖中,A 服務的提供方有 3 個實例,分別是 192.168.1.一、192.168.1.2 和 192.168.1.3,客戶端在調用目標服務 A 前,首先須要作的是負載均衡,也就是從這 3 個實例中選出 1 個來,而後再經過 RPC 把請求發送選中的目標實例。函數

![圖片描述
RPC 路由關係圖性能

RPC 框架的一個核心任務就是維護服務的路由關係,咱們能夠把服務的路由關係簡化成下圖所示的路由表。當服務提供方上線或者下線的時候,就須要更新客戶端的這張路由表。this

圖片描述

每次 RPC 調用都須要經過負載均衡器來計算目標服務的 IP 和端口號,而負載均衡器須要經過路由表獲取接口的全部路由信息,也就是說,每次 RPC 調用都須要訪問路由表,因此訪問路由表這個操做的性能要求是很高的。不過路由表對數據的一致性要求並不高,一個服務提供方從上線到反饋到客戶端的路由表裏,即使有 5 秒鐘,不少時候也都是能接受的。並且路由表是典型的讀多寫少類問題。spa

經過以上分析,你會發現一些關鍵詞:對讀的性能要求很高,讀多寫少,弱一致性。它們綜合在一塊兒,你會想到什麼呢?CopyOnWriteArrayList 和 CopyOnWriteArraySet 天生就適用這種場景啊。因此下面的示例代碼中,RouteTable 這個類內部咱們經過ConcurrentHashMap<String, CopyOnWriteArraySet<Router>>這個數據結構來描述路由表,ConcurrentHashMap 的 Key 是接口名,Value 是路由集合,這個路由集合咱們用是 CopyOnWriteArraySet。

下面咱們再來思考 Router 該如何設計,服務提供方的每一次上線、下線都會更新路由信息,這時候你有兩種選擇。

一種是經過更新 Router 的一個狀態位來標識,若是這樣作,那麼全部訪問該狀態位的地方都須要同步訪問,這樣很影響性能。

另一種就是採用 Immutability 模式,每次上線、下線都建立新的 Router 對象或者刪除對應的 Router 對象。因爲上線、下線的頻率很低,因此後者是最好的選擇。

Router 的實現代碼以下所示,是一種典型 Immutability 模式的實現,須要你注意的是咱們重寫了 equals 方法,這樣 CopyOnWriteArraySet 的 add() 和 remove() 方法才能正常工做。

// 路由信息
public final class Router{
  private final String  ip;
  private final Integer port;
  private final String  iface;
  // 構造函數
  public Router(String ip, 
      Integer port, String iface){
    this.ip = ip;
    this.port = port;
    this.iface = iface;
  }
  // 重寫 equals 方法
  public boolean equals(Object obj){
    if (obj instanceof Router) {
      Router r = (Router)obj;
      return iface.equals(r.iface) &&
             ip.equals(r.ip) &&
             port.equals(r.port);
    }
    return false;
  }
  public int hashCode() {
    // 省略 hashCode 相關代碼
  }
}
// 路由表信息
public class RouterTable {
  //Key: 接口名
  //Value: 路由集合
  ConcurrentHashMap<String, CopyOnWriteArraySet<Router>> 
    rt = new ConcurrentHashMap<>();
  // 根據接口名獲取路由表
  public Set<Router> get(String iface){
    return rt.get(iface);
  }
  // 刪除路由
  public void remove(Router router) {
    Set<Router> set=rt.get(router.iface);
    if (set != null) {
      set.remove(router);
    }
  }
  // 增長路由
  public void add(Router router) {
    Set<Router> set = rt.computeIfAbsent(
      route.iface, r -> 
        new CopyOnWriteArraySet<>());
    set.add(router);
  }
}

總結

其實 Copy-on-Write 纔是最簡單的併發解決方案。它是如此簡單,以致於 Java 中的基本數據類型 String、Integer、Long 等都是基於 Copy-on-Write 方案實現的。

Copy-on-Write 是一項很是通用的技術方案,在不少領域都有着普遍的應用。不過,它也有缺點的,那就是消耗內存,每次修改都須要複製一個新的對象出來。若是寫操做很是少,那你就能夠嘗試用一下 Copy-on-Write,效果仍是不錯的。

相關文章
相關標籤/搜索