在上一篇文章中咱們講到 Java 裏 String 這個類在實現 replace() 方法的時候,並無更改原字符串裏面 value[] 數組的內容,而是建立了一個新字符串,這種方法在解決不可變對象的修改問題時常常用到。若是你深刻地思考這個方法,你會發現它本質上是一種Copy-on-Write 方法
。所謂 Copy-on-Write,常常被縮寫爲 COW 或者 CoW,顧名思義就是寫時複製
。數組
不可變對象的寫操做每每都是使用 Copy-on-Write 方法解決的,固然 Copy-on-Write 的應用領域並不侷限於 Immutability 模式。下面咱們先簡單介紹一下 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,效果仍是不錯的。