很是硬核的技術知識-CopyOnWrite思想

「 今天聊一個很是硬核的技術知識,給你們分析一下CopyOnWrite思想是什麼,以及在Java併發包中的具體體現,包括在Kafka內核源碼中是如何運用這個思想來優化併發性能的。java

這個CopyOnWrite在面試的時候,極可能成爲面試官的一個殺手鐗把候選人給一擊必殺,也頗有可能成爲候選人拿下Offer的獨門祕籍,是相對高級的一個知識。面試

一、讀多寫少的場景下引起的問題?

你們能夠設想一下如今咱們的內存裏有一個ArrayList,這個ArrayList默認狀況下確定是線程不安全的,要是多個線程併發讀和寫這個ArrayList可能會有問題。數組

好,問題來了,咱們應該怎麼讓這個ArrayList變成線程安全的呢?安全

有一個很是簡單的辦法,對這個ArrayList的訪問都加上線程同步的控制。服務器

好比說必定要在synchronized代碼段來對這個ArrayList進行訪問,這樣的話,就能同一時間就讓一個線程來操做它了,或者是用ReadWriteLock讀寫鎖的方式來控制,均可以。微信

咱們假設就是用ReadWriteLock讀寫鎖的方式來控制對這個ArrayList的訪問。數據結構

這樣多個讀請求能夠同時執行從ArrayList裏讀取數據,可是讀請求和寫請求之間互斥,寫請求和寫請求也是互斥的。多線程

你們看看,代碼大概就是相似下面這樣:架構

public Object read() {
   lock.readLock().lock();
   // 對ArrayList讀取
   lock.readLock().unlock();
}

public void write() {
   lock.writeLock().lock();
   // 對ArrayList寫
   lock.writeLock().unlock();
}
複製代碼

你們想一想,相似上面的代碼有什麼問題呢?併發

最大的問題,其實就在於寫鎖和讀鎖的互斥。假設寫操做頻率很低,讀操做頻率很高,是寫少讀多的場景。

那麼偶爾執行一個寫操做的時候,是否是會加上寫鎖,此時大量的讀操做過來是否是就會被阻塞住,沒法執行?

這個就是讀寫鎖可能遇到的最大的問題。

二、引入 CopyOnWrite 思想解決問題

這個時候就要引入CopyOnWrite思想來解決問題了。

他的思想就是,不用加什麼讀寫鎖,鎖通通給我去掉,有鎖就有問題,有鎖就有互斥,有鎖就可能致使性能低下,你阻塞個人請求,致使個人請求都卡着不能執行。

那麼他怎麼保證多線程併發的安全性呢?

很簡單,顧名思義,利用「CopyOnWrite」的方式,這個英語翻譯成中文,大概就是**「寫數據的時候利用拷貝的副原本執行」。**

你在讀數據的時候,其實不加鎖也不要緊,你們左右都是一個讀罷了,互相沒影響。

問題主要是在寫的時候,寫的時候你既然不能加鎖了,那麼就得采用一個策略。

假如說你的ArrayList底層是一個數組來存放你的列表數據,那麼這時好比你要修改這個數組裏的數據,你就必須先拷貝這個數組的一個副本。

而後你能夠在這個數組的副本里寫入你要修改的數據,可是在這個過程當中實際上你都是在操做一個副本而已。

這樣的話,讀操做是否是能夠同時正常的執行?這個寫操做對讀操做是沒有任何的影響的吧!

你們看下面的圖,一塊兒來體會一下這個過程:

關鍵問題來了,那那個寫線程如今把副本數組給修改完了,如今怎麼才能讓讀線程感知到這個變化呢?

關鍵點來了,劃重點!這裏要配合上volatile關鍵字的使用。

筆者以前寫過文章,給你們解釋過volatile關鍵字的使用,核心就是讓一個變量被寫線程給修改以後,立馬讓其餘線程能夠讀到這個變量引用的最近的值,這就是volatile最核心的做用。

因此一旦寫線程搞定了副本數組的修改以後,那麼就能夠用volatile寫的方式,把這個副本數組賦值給volatile修飾的那個數組的引用變量了。

只要一賦值給那個volatile修飾的變量,立馬就會對讀線程可見,你們都能看到最新的數組了。

下面是JDK裏的 CopyOnWriteArrayList 的源碼。

你們看看寫數據的時候,他是怎麼拷貝一個數組副本,而後修改副本,接着經過volatile變量賦值的方式,把修改好的數組副本給更新回去,立馬讓其餘線程可見的。

// 這個數組是核心的,由於用volatile修飾了
  // 只要把最新的數組對他賦值,其餘線程立馬能夠看到最新的數組
  private transient volatile Object[] array;

  public boolean add(E e) {

      final ReentrantLock lock = this.lock;
      lock.lock();

      try {
          Object[] elements = getArray();
          int len = elements.length;

          // 對數組拷貝一個副本出來
          Object[] newElements = Arrays.copyOf(elements, len + 1);

          // 對副本數組進行修改,好比在裏面加入一個元素
          newElements[len] = e;

          // 而後把副本數組賦值給volatile修飾的變量
          setArray(newElements);
          return true;


      } finally {
          lock.unlock();
      }
  }
複製代碼

而後你們想,由於是經過副原本進行更新的,萬一要是多個線程都要同時更新呢?那搞出來多個副本會不會有問題?

固然不能多個線程同時更新了,這個時候就是看上面源碼裏,加入了lock鎖的機制,也就是同一時間只有一個線程能夠更新。

那麼更新的時候,會對讀操做有任何的影響嗎?

絕對不會,由於讀操做就是很是簡單的對那個數組進行讀而已,不涉及任何的鎖。並且只要他更新完畢對volatile修飾的變量賦值,那麼讀線程立馬能夠看到最新修改後的數組,這是volatile保證的。

這樣就完美解決了咱們以前說的讀多寫少的問題。

若是用讀寫鎖互斥的話,會致使寫鎖阻塞大量讀操做,影響併發性能。

可是若是用了CopyOnWriteArrayList,就是用空間換時間,更新的時候基於副本更新,避免鎖,而後最後用volatile變量來賦值保證可見性,更新的時候對讀線程沒有任何的影響!

三、CopyOnWrite 思想在Kafka源碼中的運用

在Kafka的內核源碼中,有這麼一個場景,客戶端在向Kafka寫數據的時候,會把消息先寫入客戶端本地的內存緩衝,而後在內存緩衝裏造成一個Batch以後再一次性發送到Kafka服務器上去,這樣有助於提高吞吐量。

話很少說,你們看下圖:

這個時候Kafka的內存緩衝用的是什麼數據結構呢?你們看源碼:

private final ConcurrentMap<topicpartition, deque<="" span="">

         batches = new CopyOnWriteMap<TopicPartition, Deque>();
複製代碼

這個數據結構就是核心的用來存放寫入內存緩衝中的消息的數據結構,要看懂這個數據結構須要對不少Kafka內核源碼裏的概念進行解釋,這裏先不展開。

可是你們關注一點,他是本身實現了一個CopyOnWriteMap,這個CopyOnWriteMap採用的就是CopyOnWrite思想。

咱們來看一下這個CopyOnWriteMap的源碼實現:

// 典型的volatile修飾普通Map
  private volatile Mapmap;

  @Override
  public synchronized V put(K k, V v) {

      // 更新的時候先建立副本,更新副本,而後對volatile變量賦值寫回去
      Mapcopy= new HashMap(this.map);
      V prev = copy.put(k, v);
      this.map = Collections.unmodifiableMap(copy);
      return prev;
  }

  @Override
  public V get(Object k) {

      // 讀取的時候直接讀volatile變量引用的map數據結構,無需鎖
      return map.get(k);

  }
複製代碼

因此Kafka這個核心數據結構在這裏之因此採用CopyOnWriteMap思想來實現,就是由於這個Map的key-value對,其實沒那麼頻繁更新。

也就是TopicPartition-Deque這個key-value對,更新頻率很低。

可是他的get操做倒是高頻的讀取請求,由於會高頻的讀取出來一個TopicPartition對應的Deque數據結構,來對這個隊列進行入隊出隊等操做,因此對於這個map而言,高頻的是其get操做。

這個時候,Kafka就採用了CopyOnWrite思想來實現這個Map,避免更新key-value的時候阻塞住高頻的讀操做,實現無鎖的效果,優化線程併發的性能。

相信你們看完這個文章,對於CopyOnWrite思想以及適用場景,包括JDK中的實現,以及在Kafka源碼中的運用,都有了一個切身的體會了。

若是你能在面試時說清楚這個思想以及他在JDK中的體現,而且還能結合知名的開源項目 Kafka 的底層源碼進一步向面試官進行闡述,面試官對你的印象確定大大的加分。

原文自:石杉的架構筆記微信公衆號

相關文章
相關標籤/搜索