【追光者系列】HikariCP原始碼分析之ConcurrentBag&J.U.C SynchronousQueue、CopyOnWriteArrayList

Charles醬發表於2019-03-01

摘要: 原創出處 微信公眾號「工匠小豬豬的技術世界」歡迎轉載,保留摘要,謝謝!

1.這是一個系列,有興趣的朋友可以持續關注
2.如果你有HikariCP使用上的問題,可以給我留言,我們一起溝通討論
3.希望大家可以提供我一些案例,我也希望可以支援你們做一些調優


【追光者系列】HikariCP原始碼分析之ConcurrentBag&J.U.C SynchronousQueue、CopyOnWriteArrayList

???關注**微信公眾號:【工匠小豬豬的技術世界】**有福利:

  1. 您對於原始碼的疑問每條留言將得到認真回覆。甚至不知道如何讀原始碼也可以請教噢
  2. 新的原始碼解析文章實時收到通知。每兩週更新一篇左右

ConcurrentBag的定義

HikariCP contains a custom lock-free collection called a ConcurrentBag. The idea was borrowed from the C# .NET ConcurrentBag class, but the internal implementation quite different. The ConcurrentBag provides…

  • A lock-free design
  • ThreadLocal caching
  • Queue-stealing
  • Direct hand-off optimizations

…resulting in a high degree of concurrency, extremely low latency, and minimized occurrences of false-sharing.

https://en.wikipedia.org/wiki/False_sharing

  • CopyOnWriteArrayList:負責存放ConcurrentBag中全部用於出借的資源
  • ThreadLocal:用於加速執行緒本地化資源訪問
  • SynchronousQueue:用於存在資源等待執行緒時的第一手資源交接

ConcurrentBag取名來源於C# .NET的同名類,但是實現卻不一樣。它是一個lock-free集合,在連線池(多執行緒資料互動)的實現上具有比LinkedBlockingQueue和LinkedTransferQueue更優越的併發讀寫效能。

ConcurrentBag原始碼解析

ConcurrentBag內部同時使用了ThreadLocal和CopyOnWriteArrayList來儲存元素,其中CopyOnWriteArrayList是執行緒共享的。ConcurrentBag採用了queue-stealing的機制獲取元素:首先嚐試從ThreadLocal中獲取屬於當前執行緒的元素來避免鎖競爭,如果沒有可用元素則掃描公共集合、再次從共享的CopyOnWriteArrayList中獲取。(ThreadLocal列表中沒有被使用的items在借用執行緒沒有屬於自己的時候,是可以被“竊取”的)
ThreadLocal和CopyOnWriteArrayList在ConcurrentBag中都是成員變數,執行緒間不共享,避免了偽共享(false sharing)的發生。
其使用專門的AbstractQueuedLongSynchronizer來管理跨執行緒訊號,這是一個”lock-less“的實現。
這裡要特別注意的是,ConcurrentBag中通過borrow方法進行資料資源借用,通過requite方法進行資源回收,注意其中borrow方法只提供物件引用,不移除物件。所以從bag中“借用”的items實際上並沒有從任何集合中刪除,因此即使引用廢棄了,垃圾收集也不會發生。因此使用時通過borrow取出的物件必須通過requite方法進行放回,否則會導致記憶體洩露,只有”remove”方法才能完全從bag中刪除一個物件。

好了,我們一起看一下ConcurrentBag原始碼概覽:

【追光者系列】HikariCP原始碼分析之ConcurrentBag&J.U.C SynchronousQueue、CopyOnWriteArrayList

上節提過,CopyOnWriteArrayList負責存放ConcurrentBag中全部用於出借的資源,就是private final CopyOnWriteArrayList sharedList; 如下圖所示,sharedList中的資源通過add方法新增,remove方法出借

【追光者系列】HikariCP原始碼分析之ConcurrentBag&J.U.C SynchronousQueue、CopyOnWriteArrayList

add方法向bag中新增bagEntry物件,讓別人可以借用

   /**
    * Add a new object to the bag for others to borrow.
    *
    * @param bagEntry an object to add to the bag
    */
   public void add(final T bagEntry)
   {
      if (closed) {
         LOGGER.info("ConcurrentBag has been closed, ignoring add()");
         throw new IllegalStateException("ConcurrentBag has been closed, ignoring add()");
      }


      sharedList.add(bagEntry);//新新增的資源優先放入CopyOnWriteArrayList


      // spin until a thread takes it or none are waiting
      // 當有等待資源的執行緒時,將資源交到某個等待執行緒後才返回(SynchronousQueue)
      while (waiters.get() > 0 && !handoffQueue.offer(bagEntry)) {
         yield();
      }
   }

複製程式碼

remove方法用來從bag中刪除一個bageEntry,該方法只能在borrow(long, TimeUnit)和reserve(T)時被使用

   /**
    * Remove a value from the bag.  This method should only be called
    * with objects obtained by <code>borrow(long, TimeUnit)</code> or <code>reserve(T)</code>
    *
    * @param bagEntry the value to remove
    * @return true if the entry was removed, false otherwise
    * @throws IllegalStateException if an attempt is made to remove an object
    *         from the bag that was not borrowed or reserved first
    */
   public boolean remove(final T bagEntry)
   {
   // 如果資源正在使用且無法進行狀態切換,則返回失敗
      if (!bagEntry.compareAndSet(STATE_IN_USE, STATE_REMOVED) && !bagEntry.compareAndSet(STATE_RESERVED, STATE_REMOVED) && !closed) {
         LOGGER.warn("Attempt to remove an object from the bag that was not borrowed or reserved: {}", bagEntry);
         return false;
      }

      final boolean removed = sharedList.remove(bagEntry);// 從CopyOnWriteArrayList中移出
      if (!removed && !closed) {
         LOGGER.warn("Attempt to remove an object from the bag that does not exist: {}", bagEntry);
      }

      return removed;
   }
複製程式碼

ConcurrentBag中通過borrow方法進行資料資源借用

/**
    * The method will borrow a BagEntry from the bag, blocking for the
    * specified timeout if none are available.
    *
    * @param timeout how long to wait before giving up, in units of unit
    * @param timeUnit a <code>TimeUnit</code> determining how to interpret the timeout parameter
    * @return a borrowed instance from the bag or null if a timeout occurs
    * @throws InterruptedException if interrupted while waiting
    */
   public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
   {
      // Try the thread-local list first
      // 優先檢視有沒有可用的本地化的資源
      final List<Object> list = threadList.get();
      for (int i = list.size() - 1; i >= 0; i--) {
         final Object entry = list.remove(i);
         @SuppressWarnings("unchecked")
         final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;
         if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            return bagEntry;
         }
      }

      // Otherwise, scan the shared list ... then poll the handoff queue
      final int waiting = waiters.incrementAndGet();
      try {
      // 當無可用本地化資源時,遍歷全部資源,檢視是否存在可用資源
      // 因此被一個執行緒本地化的資源也可能被另一個執行緒“搶走”
         for (T bagEntry : sharedList) {
            if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
               // If we may have stolen another waiter`s connection, request another bag add.
               if (waiting > 1) {
               // 因為可能“搶走”了其他執行緒的資源,因此提醒包裹進行資源新增
                  listener.addBagItem(waiting - 1);
               }
               return bagEntry;
            }
         }

         listener.addBagItem(waiting);

         timeout = timeUnit.toNanos(timeout);
         do {
            final long start = currentTime();
            // 當現有全部資源全部在使用中,等待一個被釋放的資源或者一個新資源
            final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
            if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
               return bagEntry;
            }

            timeout -= elapsedNanos(start);
         } while (timeout > 10_000);

         return null;
      }
      finally {
         waiters.decrementAndGet();
      }
   }
複製程式碼
/**
    * This method will return a borrowed object to the bag.  Objects
    * that are borrowed from the bag but never "requited" will result
    * in a memory leak.
    *
    * @param bagEntry the value to return to the bag
    * @throws NullPointerException if value is null
    * @throws IllegalStateException if the bagEntry was not borrowed from the bag
    */
   public void requite(final T bagEntry)
   {
   // 將狀態轉為未在使用
      bagEntry.setState(STATE_NOT_IN_USE);

// 判斷是否存在等待執行緒,若存在,則直接轉手資源
      for (int i = 0; waiters.get() > 0; i++) {
         if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {
            return;
         }
         else if ((i & 0xff) == 0xff) {
            parkNanos(MICROSECONDS.toNanos(10));
         }
         else {
            yield();
         }
      }

 // 否則,進行資源本地化
      final List<Object> threadLocalList = threadList.get();
      threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
   }
複製程式碼

上述程式碼中的 weakThreadLocals 是用來判斷是否使用弱引用,通過下述方法初始化:

/**
    * Determine whether to use WeakReferences based on whether there is a
    * custom ClassLoader implementation sitting between this class and the
    * System ClassLoader.
    *
    * @return true if we should use WeakReferences in our ThreadLocals, false otherwise
    */
   private boolean useWeakThreadLocals()
   {
      try {
      // 人工指定是否使用弱引用,但是官方不推薦進行自主設定。
         if (System.getProperty("com.zaxxer.hikari.useWeakReferences") != null) {   // undocumented manual override of WeakReference behavior
            return Boolean.getBoolean("com.zaxxer.hikari.useWeakReferences");
         }

// 預設通過判斷初始化的ClassLoader是否是系統的ClassLoader來確定
         return getClass().getClassLoader() != ClassLoader.getSystemClassLoader();
      }
      catch (SecurityException se) {
         return true;
      }
   }
複製程式碼

SynchronousQueue

SynchronousQueue主要用於存在資源等待執行緒時的第一手資源交接,如下圖所示:

【追光者系列】HikariCP原始碼分析之ConcurrentBag&J.U.C SynchronousQueue、CopyOnWriteArrayList

在hikariCP中,選擇的是公平模式 this.handoffQueue = new SynchronousQueue<>(true);

公平模式總結下來就是:隊尾匹配隊頭出隊,先進先出,體現公平原則。

SynchronousQueue是一個是一個無儲存空間的阻塞佇列(是實現newFixedThreadPool的核心),非常適合做交換工作,生產者的執行緒和消費者的執行緒同步以傳遞某些資訊、事件或者任務。
因為是無儲存空間的,所以與其他阻塞佇列實現不同的是,這個阻塞peek方法直接返回null,無任何其他操作,其他的方法與阻塞佇列的其他方法一致。這個佇列的特點是,必須先呼叫take或者poll方法,才能使用off,add方法。

作為BlockingQueue中的一員,SynchronousQueue與其他BlockingQueue有著不同特性(來自明姐http://cmsblogs.com/?p=2418):

  • SynchronousQueue沒有容量。與其他BlockingQueue不同,SynchronousQueue是一個不儲存元素的BlockingQueue。每一個put操作必須要等待一個take操作,否則不能繼續新增元素,反之亦然。
  • 因為沒有容量,所以對應 peek, contains, clear, isEmpty … 等方法其實是無效的。例如clear是不執行任何操作的,contains始終返回false,peek始終返回null。
  • SynchronousQueue分為公平和非公平,預設情況下采用非公平性訪問策略,當然也可以通過建構函式來設定為公平性訪問策略(為true即可)。
  • 若使用 TransferQueue, 則佇列中永遠會存在一個 dummy node。

SynchronousQueue提供了兩個建構函式:

    public SynchronousQueue() {
        this(false);
    }

    public SynchronousQueue(boolean fair) {
        // 通過 fair 值來決定公平性和非公平性
        // 公平性使用TransferQueue,非公平性採用TransferStack
        transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
    }
複製程式碼

TransferQueue、TransferStack繼承Transferer,Transferer為SynchronousQueue的內部類,它提供了一個方法transfer(),該方法定義了轉移資料的規範

abstract static class Transferer<E> {
        abstract E transfer(E e, boolean timed, long nanos);
    }
複製程式碼

transfer()方法主要用來完成轉移資料的,如果e != null,相當於將一個資料交給消費者,如果e == null,則相當於從一個生產者接收一個消費者交出的資料。

SynchronousQueue採用佇列TransferQueue來實現公平性策略,採用堆疊TransferStack來實現非公平性策略,他們兩種都是通過連結串列實現的,其節點分別為QNode,SNode。TransferQueue和TransferStack在SynchronousQueue中扮演著非常重要的作用,SynchronousQueue的put、take操作都是委託這兩個類來實現的。

公平模式

公平模式底層使用的TransferQueue內部佇列,一個head和tail指標,用於指向當前正在等待匹配的執行緒節點。 (來自https://blog.csdn.net/yanyan19880509/article/details/52562039
初始化時,TransferQueue的狀態如下:

【追光者系列】HikariCP原始碼分析之ConcurrentBag&J.U.C SynchronousQueue、CopyOnWriteArrayList

接著我們進行一些操作:

1、執行緒put1執行 put(1)操作,由於當前沒有配對的消費執行緒,所以put1執行緒入佇列,自旋一小會後睡眠等待,這時佇列狀態如下:

【追光者系列】HikariCP原始碼分析之ConcurrentBag&J.U.C SynchronousQueue、CopyOnWriteArrayList

2、接著,執行緒put2執行了put(2)操作,跟前面一樣,put2執行緒入佇列,自旋一小會後睡眠等待,這時佇列狀態如下:

【追光者系列】HikariCP原始碼分析之ConcurrentBag&J.U.C SynchronousQueue、CopyOnWriteArrayList

3、這時候,來了一個執行緒take1,執行了 take操作,由於tail指向put2執行緒,put2執行緒跟take1執行緒配對了(一put一take),這時take1執行緒不需要入隊,但是請注意了,這時候,要喚醒的執行緒並不是put2,而是put1。為何? 大家應該知道我們現在講的是公平策略,所謂公平就是誰先入隊了,誰就優先被喚醒,我們的例子明顯是put1應該優先被喚醒。至於讀者可能會有一個疑問,明明是take1執行緒跟put2執行緒匹配上了,結果是put1執行緒被喚醒消費,怎麼確保take1執行緒一定可以和次首節點(head.next)也是匹配的呢?其實大家可以拿個紙畫一畫,就會發現真的就是這樣的。
公平策略總結下來就是:隊尾匹配隊頭出隊。
執行後put1執行緒被喚醒,take1執行緒的 take()方法返回了1(put1執行緒的資料),這樣就實現了執行緒間的一對一通訊,這時候內部狀態如下:

【追光者系列】HikariCP原始碼分析之ConcurrentBag&J.U.C SynchronousQueue、CopyOnWriteArrayList

4、最後,再來一個執行緒take2,執行take操作,這時候只有put2執行緒在等候,而且兩個執行緒匹配上了,執行緒put2被喚醒,
take2執行緒take操作返回了2(執行緒put2的資料),這時候佇列又回到了起點,如下所示:

【追光者系列】HikariCP原始碼分析之ConcurrentBag&J.U.C SynchronousQueue、CopyOnWriteArrayList

以上便是公平模式下,SynchronousQueue的實現模型。總結下來就是:隊尾匹配隊頭出隊,先進先出,體現公平原則。

非公平模式

還是使用跟公平模式下一樣的操作流程,對比兩種策略下有何不同。非公平模式底層的實現使用的是TransferStack,
一個棧,實現中用head指標指向棧頂,接著我們看看它的實現模型:

1、執行緒put1執行 put(1)操作,由於當前沒有配對的消費執行緒,所以put1執行緒入棧,自旋一小會後睡眠等待,這時棧狀態如下:

【追光者系列】HikariCP原始碼分析之ConcurrentBag&J.U.C SynchronousQueue、CopyOnWriteArrayList

2、接著,執行緒put2再次執行了put(2)操作,跟前面一樣,put2執行緒入棧,自旋一小會後睡眠等待,這時棧狀態如下:

【追光者系列】HikariCP原始碼分析之ConcurrentBag&J.U.C SynchronousQueue、CopyOnWriteArrayList

3、這時候,來了一個執行緒take1,執行了take操作,這時候發現棧頂為put2執行緒,匹配成功,但是實現會先把take1執行緒入棧,然後take1執行緒迴圈執行匹配put2執行緒邏輯,一旦發現沒有併發衝突,就會把棧頂指標直接指向 put1執行緒

【追光者系列】HikariCP原始碼分析之ConcurrentBag&J.U.C SynchronousQueue、CopyOnWriteArrayList

4、最後,再來一個執行緒take2,執行take操作,這跟步驟3的邏輯基本是一致的,take2執行緒入棧,然後在迴圈中匹配put1執行緒,最終全部匹配完畢,棧變為空,恢復初始狀態,如下圖所示:

【追光者系列】HikariCP原始碼分析之ConcurrentBag&J.U.C SynchronousQueue、CopyOnWriteArrayList

從上面流程看出,雖然put1執行緒先入棧了,但是卻是後匹配,這就是非公平的由來。

CopyOnWriteArrayList

CopyOnWriteArrayList負責存放ConcurrentBag中全部用於出借的資源。(引自http://www.importnew.com/25034.html)

CopyOnWriteArrayList,顧名思義,Write的時候總是要Copy,也就是說對於任何可變的操作(add、set、remove)都是伴隨複製這個動作的,是ArrayList 的一個執行緒安全的變體。

A thread-safe variant of ArrayList in which all mutative operations (add, set, and so on) are implemented by making a fresh copy of the underlying array.This is ordinarily too costly, but may be more efficient than alternatives when traversal operations vastly outnumber mutations, and is useful when you cannot or don`t want to synchronize traversals, yet need to preclude interference among concurrent threads. The “snapshot” style iterator method uses a reference to the state of the array at the point that the iterator was created. This array never changes during the lifetime of the iterator, so interference is impossible and the iterator is guaranteed not to throw ConcurrentModificationException. The iterator will not reflect additions, removals, or changes to the list since the iterator was created. Element-changing operations on iterators themselves (remove, set, and add) are not supported. These methods throw UnsupportedOperationException. All elements are permitted, including null.

【追光者系列】HikariCP原始碼分析之ConcurrentBag&J.U.C SynchronousQueue、CopyOnWriteArrayList
【追光者系列】HikariCP原始碼分析之ConcurrentBag&J.U.C SynchronousQueue、CopyOnWriteArrayList

CopyOnWriteArrayList的add操作的原始碼如下:

 public boolean add(E e) {
    //1、先加鎖
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        //2、拷貝陣列
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //3、將元素加入到新陣列中
        newElements[len] = e;
        //4、將array引用指向到新陣列
        setArray(newElements);
        return true;
    } finally {
       //5、解鎖
        lock.unlock();
    }
}
複製程式碼

一次add大致經歷了幾個步驟:

1、加鎖
2、拿到原陣列,得到新陣列的大小(原陣列大小+1),例項化出一個新的陣列來
3、把原陣列的元素複製到新陣列中去
4、新陣列最後一個位置設定為待新增的元素(因為新陣列的大小是按照原陣列大小+1來的)
5、把Object array引用指向新陣列
6、解鎖

插入、刪除、修改操作也都是一樣,每一次的操作都是以對Object[] array進行一次複製為基礎的

由於所有的寫操作都是在新陣列進行的,這個時候如果有執行緒併發的寫,則通過鎖來控制,如果有執行緒併發的讀,則分幾種情況:
1、如果寫操作未完成,那麼直接讀取原陣列的資料;
2、如果寫操作完成,但是引用還未指向新陣列,那麼也是讀取原陣列資料;
3、如果寫操作完成,並且引用已經指向了新的陣列,那麼直接從新陣列中讀取資料。

可見,CopyOnWriteArrayList的讀操作是可以不用加鎖的。

常用的List有ArrayList、LinkedList、Vector,其中前兩個是執行緒非安全的,最後一個是執行緒安全的。Vector雖然是執行緒安全的,但是隻是一種相對的執行緒安全而不是絕對的執行緒安全,它只能夠保證增、刪、改、查的單個操作一定是原子的,不會被打斷,但是如果組合起來用,並不能保證執行緒安全性。比如就像上面的執行緒1在遍歷一個Vector中的元素、執行緒2在刪除一個Vector中的元素一樣,勢必產生併發修改異常,也就是fail-fast。

所以這就是選擇CopyOnWriteArrayList這個併發元件的原因,CopyOnWriteArrayList如何做到執行緒安全的呢?

CopyOnWriteArrayList使用了一種叫寫時複製的方法,當有新元素新增到CopyOnWriteArrayList時,先從原有的陣列中拷貝一份出來,然後在新的陣列做寫操作,寫完之後,再將原來的陣列引用指向到新陣列。

當有新元素加入的時候,如下圖,建立新陣列,並往新陣列中加入一個新元素,這個時候,array這個引用仍然是指向原陣列的。

【追光者系列】HikariCP原始碼分析之ConcurrentBag&J.U.C SynchronousQueue、CopyOnWriteArrayList

當元素在新陣列新增成功後,將array這個引用指向新陣列。

【追光者系列】HikariCP原始碼分析之ConcurrentBag&J.U.C SynchronousQueue、CopyOnWriteArrayList

CopyOnWriteArrayList的整個add操作都是在鎖的保護下進行的。
這樣做是為了避免在多執行緒併發add的時候,複製出多個副本出來,把資料搞亂了,導致最終的陣列資料不是我們期望的。

CopyOnWriteArrayList反映的是三個十分重要的分散式理念:

1)讀寫分離
我們讀取CopyOnWriteArrayList的時候讀取的是CopyOnWriteArrayList中的Object[] array,但是修改的時候,操作的是一個新的Object[] array,讀和寫操作的不是同一個物件,這就是讀寫分離。這種技術資料庫用的非常多,在高併發下為了緩解資料庫的壓力,即使做了快取也要對資料庫做讀寫分離,讀的時候使用讀庫,寫的時候使用寫庫,然後讀庫、寫庫之間進行一定的同步,這樣就避免同一個庫上讀、寫的IO操作太多
2)最終一致
對CopyOnWriteArrayList來說,執行緒1讀取集合裡面的資料,未必是最新的資料。因為執行緒2、執行緒3、執行緒4四個執行緒都修改了CopyOnWriteArrayList裡面的資料,但是執行緒1拿到的還是最老的那個Object[] array,新新增進去的資料並沒有,所以執行緒1讀取的內容未必準確。不過這些資料雖然對於執行緒1是不一致的,但是對於之後的執行緒一定是一致的,它們拿到的Object[] array一定是三個執行緒都操作完畢之後的Object array[],這就是最終一致。最終一致對於分散式系統也非常重要,它通過容忍一定時間的資料不一致,提升整個分散式系統的可用性與分割槽容錯性。當然,最終一致並不是任何場景都適用的,像火車站售票這種系統使用者對於資料的實時性要求非常非常高,就必須做成強一致性的。
3)使用另外開闢空間的思路,來解決併發衝突

缺點:
1、因為CopyOnWrite的寫時複製機制,所以在進行寫操作的時候,記憶體裡會同時駐紮兩個物件的記憶體,舊的物件和新寫入的物件(注意:在複製的時候只是複製容器裡的引用,只是在寫的時候會建立新物件新增到新容器裡,而舊容器的物件還在使用,所以有兩份物件記憶體)。如果這些物件佔用的記憶體比較大,比如說200M左右,那麼再寫入100M資料進去,記憶體就會佔用300M,那麼這個時候很有可能造成頻繁的Yong GC和Full GC。之前某系統中使用了一個服務由於每晚使用CopyOnWrite機制更新大物件,造成了每晚15秒的Full GC,應用響應時間也隨之變長。針對記憶體佔用問題,可以通過壓縮容器中的元素的方法來減少大物件的記憶體消耗,比如,如果元素全是10進位制的數字,可以考慮把它壓縮成36進位制或64進位制。或者不使用CopyOnWrite容器,而使用其他的併發容器,如ConcurrentHashMap。
2、不能用於實時讀的場景,像拷貝陣列、新增元素都需要時間,所以呼叫一個set操作後,讀取到資料可能還是舊的,雖CopyOnWriteArrayList 能做到最終一致性,但是還是沒法滿足實時性要求;
3.資料一致性問題。CopyOnWrite容器只能保證資料的最終一致性,不能保證資料的實時一致性。所以如果你希望寫入的的資料,馬上能讀到,請不要使用CopyOnWrite容器。關於C++的STL中,曾經也有過Copy-On-Write的玩法,參見陳皓的《C++ STL String類中的Copy-On-Write》,後來,因為有很多執行緒安全上的事,就被去掉了。https://blog.csdn.net/haoel/article/details/24058

隨著CopyOnWriteArrayList中元素的增加,CopyOnWriteArrayList的修改代價將越來越昂貴,因此,CopyOnWriteArrayList 合適讀多寫少的場景,不過這類慎用
因為誰也沒法保證CopyOnWriteArrayList 到底要放置多少資料,萬一資料稍微有點多,每次add/set都要重新複製陣列,這個代價實在太高昂了。在高效能的網際網路應用中,這種操作分分鐘引起故障。**CopyOnWriteArrayList適用於讀操作遠多於修改操作的併發場景中。**而HikariCP就是這種場景。

還有比如白名單,黑名單,商品類目的訪問和更新場景,假如我們有一個搜尋網站,使用者在這個網站的搜尋框中,輸入關鍵字搜尋內容,但是某些關鍵字不允許被搜尋。這些不能被搜尋的關鍵字會被放在一個黑名單當中,黑名單每天晚上更新一次。當使用者搜尋時,會檢查當前關鍵字在不在黑名單當中,如果在,則提示不能搜尋。

但是使用CopyOnWriteMap需要注意兩件事情:

  1. 減少擴容開銷。根據實際需要,初始化CopyOnWriteMap的大小,避免寫時CopyOnWriteMap擴容的開銷。

  2. 使用批量新增。因為每次新增,容器每次都會進行復制,所以減少新增次數,可以減少容器的複製次數。

參考資料

http://www.cnblogs.com/taisenki/p/7699667.html

PLC程式設計| 從原理圖到程式的4個經典例項詳解


https://blog.csdn.net/yanyan19880509/article/details/52562039
http://www.importnew.com/25034.html
https://blog.csdn.net/linsongbin1/article/details/54581787

相關文章