高級併發編程系列十七(一文搞懂CopyOnWriteArrayList)

1.考考你

週末好!今天我要給你分享的是CopyOnWriteArrayList。關於CopyOnWriteArrayList可能你還不太熟悉,由於它在咱們平常開發中,確實用的不算多。它有它的特殊應用場景,也有它比較明顯的侷限性。java

我之因此專門經過一篇文章給你分享,是想經過CopyOnWriteArrayList把寫時複製的思想分享給你。在你看具體內容前,讓咱們一塊兒先思考這麼幾個問題:編程

  • CopyOnWriteArrayList類名稱中,有咱們熟悉的ArrayList,那麼平常開發使用ArrayList的時候,有什麼你須要注意的地方嗎?設計模式

  • CopyOnWrite中文翻譯過來,是寫時複製,到底什麼是寫時複製呢?數組

  • 關於寫時複製的思想,在什麼場景下適合應用,有什麼須要注意的地方嗎?安全

帶着以上幾個問題,讓咱們一塊兒開始今天的內容吧。數據結構

2.案例

2.1.ArrayList踩過的坑

2.1.1.同祖宗,不相忘

CopyOnWriteArrayList類名稱中,包含有ArrayList,這代表它們之間具備血緣關係,起源於一個老祖宗,咱們先來看類圖:架構

2.1.2.ArrayList不能這麼用

經過類圖咱們看到CopyOnWriteArrayList、ArrayList都實現了相同的接口。爲了方便你更好的理解CopyOnWriteArrayList,咱們先從ArrayList講起。併發

接下來我將經過平常開發中使用ArrayList,我將給你分享須要有意識避開的一些案例。app

咱們知道ArrayList底層是基於數組數據結構實現,它的特性是:擁有數組的一切特性,且支持動態擴容。那麼咱們使用ArrayList,實際上是把它做爲容器來使用,對於容器,你能想到都有哪些常規操做嗎?源碼分析

  • 將元素放入容器中

  • 更新容器中的某個元素

  • 刪除容器中的某個元素

  • 獲取容器中的某個元素

  • 循環遍歷容器中的元素

以上都是咱們在項目中,使用容器時的一些高頻操做。對於每一個操做,我就不帶着你一一演示了,你應該都很熟悉。這裏咱們重點關注循環遍歷容器中的元素這個操做。

咱們知道容器的循環遍歷操做,能夠經過for循環遍歷,還能夠經過迭代器循環遍歷。經過上面的類圖,咱們知道ArrayList頂層實現了Iterable接口,因此它是支持迭代器操做的,這裏迭代器,即應用了迭代器設計模式。關於設計模式的內容,咱們暫且不去深究,時間容許的話,我將在下一個系列與你分享我理解的面向對象編程、設計原則、設計思想與設計模式。

接下來我經過ArrayList迭代器遍歷過程當中,須要留意的一些地方。咱們直接上代碼(show me the code):

package com.anan.edu.common.newthread.collection;

import java.util.ArrayList;
import java.util.Iterator;

/**
 * 演示ArrayList迭代器遍歷時,須要注意的細節
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2020/12/26 10:50
 */
public class ShowMeArrayList {

    public static void main(String[] args) {
        // 建立一個ArrayList
        ArrayList<String> list = new ArrayList<>();
        
        // 添加元素
        list.add("zhangsan");
        list.add("lisi");
        list.add("wangwu");

        /*
        * 正常循環迭代輸出
        * */
        Iterator<String> iter = list.iterator();
        while(iter.hasNext()){
            System.out.println("當前從容器中獲取的人是:"+ iter.next());
        }

    }

}

執行結果:

當前從容器中獲取的人是:zhangsan
當前從容器中獲取的人是:lisi
當前從容器中獲取的人是:wangwu

經過建立ArrayList實例,添加三個元素:zhangsan 、lisi、wangwu,並經過迭代器進行遍歷輸出。這樣一來咱們就準備好了案例基礎案例代碼。

接下來咱們作一些演化操做:

  • 在遍歷的過程當中,經過ArrayList添加、或者刪除集合中的元素

  • 在遍歷的過程當中,經過迭代器Iterator刪除集合中的元素

show me code:

/*
* 遍歷過程當中,經過Iterator實例:刪除元素
* 預期結果:正常執行
* */
Iterator<String> iter = list.iterator();
while(iter.hasNext()){
   // 若是當前遍歷到lisi,咱們將lisi從集合中刪除
   String name = iter.next();
   if("lisi".equals(name)){
        iter.remove();// 不會拋出異常   why?
    }
       System.out.println("當前從容器中獲取的人是:"+ name);
}
System.out.println("刪除元素後,集合中還有元素:" + list);  

// 執行結果
當前從容器中獲取的人是:zhangsan
當前從容器中獲取的人是:lisi
當前從容器中獲取的人是:wangwu
刪除元素後,集合中還有元素:[zhangsan, wangwu]
 
/******************************************************/    
/*
* 遍歷過程當中,經過ArrayList實例:添加、或者刪除元素
* 預期結果:遍歷拋出異常
* */
Iterator<String> iter = list.iterator();
while(iter.hasNext()){
    // 若是當前遍歷到lisi,咱們向集合中添加:小明
    String name = iter.next();
    if("lisi".equals(name)){
       list.add("小明");// 這行代碼後,繼續迭代器拋出異常  why?
    }
     System.out.println("當前從容器中獲取的人是:"+ name);
}

// 執行結果
當前從容器中獲取的人是:zhangsan
Exception in thread "main" java.util.ConcurrentModificationException
當前從容器中獲取的人是:lisi
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at com.anan.edu.common.newthread.collection.ShowMeArrayList.main(ShowMeArrayList.java:31)

2.1.3.背後的邏輯

上面咱們經過案例演示了ArrayList在迭代操做的時候,經過迭代器刪除元素操做,程序不會拋出異常;經過ArrayList添加、刪除,都會引發後續的迭代操做拋出異常。你知道這背後的邏輯嗎?

關於這個問題,我從兩個角度給你分享:

  • 爲何迭代器操做中,不容許向原集合中添加、刪除元素?

  • ArrayList中,是如何控制迭代操做中,如何檢測原集合是否被添加、刪除操做過?

爲了講清楚這個問題,咱們從圖開始(一圖勝千言):

 

高清楚爲何迭代器操做中,不容許向原集合中添加、刪除元素?這個問題後,咱們再進一步看ArrayList是如何檢測控制,在迭代過程當中,原集合有添加、或者刪除操做這個問題。

這裏我將帶你看一下源代碼,這也是我建議你應該要常常作的事情,養成看源代碼習慣,咱們常說:源碼之下無祕密。

/*
*ArrayList的迭代器,是一個內部類
*/
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator<E> {
    // 迭代器內部遊標,標識下一個待遍歷元素的數組下標
    int cursor;       // index of next element to return
    // 標識已經迭代的最後一個元素的數組下標
    int lastRet = -1; // index of last element returned; -1 if no such
    
    // 注意:這個變量很重要,它是整個迭代器迭代過程當中
    // 標識原集合被添加、刪除操做的次數
    // 初始值是集合中的成員變量:modCount(集合被添加、刪除操做計數值)
    int expectedModCount = modCount;

   Itr() {}
    ........................
}

/*
*迭代器 hasNext方法
*/
public boolean hasNext() {
    // 簡單判斷 cursor是否等於 size
    // 相等,則遍歷結束
    // 不相等,則繼續遍歷
   return cursor != size;
}

/*
*迭代器 next方法
*/
public E next() {
  // 關鍵代碼:檢查原集合是否被添加、或者刪除操做
  // 若是有添加,或者刪除操做,那麼expectedModCount != modCount
  // 拋出異常
  checkForComodification();
  int i = cursor;
  if (i >= size)
     throw new NoSuchElementException();
  Object[] elementData = ArrayList.this.elementData;
  if (i >= elementData.length)
       throw new ConcurrentModificationException();
  cursor = i + 1;
   return (E) elementData[lastRet = i];
}

/*
*迭代器 checkForComodification方法
*/
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

經過ArrayList內部類迭代器Itr的源碼分析,咱們看到迭代器的源碼實現很是簡答,而且恭喜你!在不知覺中你還學會了迭代器設計模式的實現。

最後咱們再經過查看ArrayList中add、remove方法的源碼,解惑modCount成員變量的問題:

/*
*ArrayList 的add方法
*/
/**
* Appends the specified element to the end of this list.
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
  // 註釋說了:會將modCount成員變量加1 
  //繼續看ensureCapacityInternal方法
  ensureCapacityInternal(size + 1);  // Increments modCount!!
  elementData[size++] = e;
  return true;
}

/*
*ArrayList 的ensureCapacityInternal方法
*重點是ensureExplicitCapacity方法
*/
private void ensureExplicitCapacity(int minCapacity) {
  // 將modCount變量加1
  modCount++;

  // overflow-conscious code
  if (minCapacity - elementData.length > 0)
      // 擴容操做,留給你去看了
      grow(minCapacity);
}


/*
*ArrayList 的remove方法
*/
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices).
*
* @param index the index of the element to be removed
* @return the element that was removed from the list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
  rangeCheck(index);

  // 將modCount變量加1
  modCount++;
  E oldValue = elementData(index);

  int numMoved = size - index - 1;
  if (numMoved > 0)
     System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
   elementData[--size] = null; // clear to let GC do its work

   return oldValue;
}

經過圖、和源碼分析的方式,如今你應該能夠更好的理解ArrayList、和它的內部迭代器Itr,而且在你的項目中能夠很好的使用ArrayList。

這也是我重點想要分享給你的地方:持續學習,作到知其然,且知其因此然,一種專研的精神。年輕人少刷點抖音、快手、少看點直播,這些東西除了消耗掉你的精氣神外,不會給你帶來任何正向價值的東西

2.2.CopyOnWriteArrayList詳解

2.2.1.CopyOnWriteArrayList初體驗

爲了方便你理解CopyOnWriteArrayList,我煞費苦心的帶你一路分析ArrayList。如今讓咱們先直觀的看一下CopyOnWriteArrayList。仍是經過前面的案例,即迭代器迭代過程當中,給原集合添加,或者刪除元素。

咱們經過ArrayList演示案例的時候,你還記得吧,會拋出異常,至於異常的緣由在前面的內容中,我帶你一塊兒作了專門的分析。若是你不記得了,建議回頭再去看一看

如今我重點經過CopyOnWriteArrayList來演示案例,看在相同的場景下,是否還會拋出異常?你須要重點關心一下這個地方

show me the code:

package com.anan.edu.common.newthread.collection;

import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * 演示CopyOnWriteArrayList迭代器遍歷時,須要注意的細節
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2020/12/26 10:50
 */
public class ShowMeCopyOnWriteArrayList {

   public static void main(String[] args) {
     // 建立一個CopyOnWriteArrayList
     CopyOnWriteArrayList<String> list =new CopyOnWriteArrayList<>();

     // 添加元素
     list.add("zhangsan");
     list.add("lisi");
     list.add("wangwu");

     /*
     * 遍歷過程當中,經過CopyOnWriteArrayList實例:添加、或者刪除元素
     * 預期結果:正常執行
     * */
     Iterator<String> iter = list.iterator();
     while(iter.hasNext()){
        // 若是當前遍歷到lisi,咱們向集合中添加:小明
        String name = iter.next();
        if("lisi".equals(name)){
           list.add("小明");// 不會拋出異常   why?
        }
        System.out.println("當前從容器中獲取的人是:"+ name);
     }
     System.out.println("添加元素後,集合中還有元素:" + list);

   }

}

執行結果:

當前從容器中獲取的人是:zhangsan
當前從容器中獲取的人是:lisi
當前從容器中獲取的人是:wangwu
添加元素後,集合中還有元素:[zhangsan, lisi, wangwu, 小明]

經過執行結果看到,使用CopyOnWriteArrayList,在迭代器迭代過程當中,向原集合中添加了一個新的元素:小明。迭代器繼續迭代並不會拋出異常,且最後打印結果顯示小明確認已經添加到了集合中

對於這個結果,你是否是感到多少有點意外!感受與ArrayList不是一個套路對吧。它究竟是如何實現的呢?

2.2.2.寫時複製思想

 

剛纔咱們經過CopyOnWriteArrayList,與ArrayList作了案例演示的對比,發現它們在執行結果上有很大的不同。結果差別的本質緣由是CopyOnWriteArrayList類名稱中的關鍵字:CopyOnWrite,中文翻譯過來是:寫時複製

到底什麼是寫時複製呢?所謂寫時複製,它直觀的含義是:

  • 我已經有了一個集合A,當須要往集合A中添加一個元素,或者刪除一個元素的時候

  • 保持A集合不變,從A集合複製一個新的集合B

  • 對應向新集合B中添加、或者刪除元素,操做完畢後,將A指向新的B集合,即用新的集合,替換舊的集合

     

你看這就是寫時複製的思想,理解起來並不困難。這樣作有什麼好處呢?好處就是當咱們經過迭代器訪問集合的時候,咱們能夠同時容許向集合中添加、刪除集合元素,有效避免了訪問集合(讀操做),與更新集合(寫操做)的衝突,最大化實現了集合的併發訪問性能

那麼關於CopyOnWriteArrayList,它是如何最大化提高併發訪問能力呢?它的實現原理並不複雜,既然是併發訪問,線程安全的問題不可迴避,你應該也想到了,首先加鎖是必須的。

除了加鎖,還須要考慮提高併發訪問的能力,如何提高?實現也很簡單,針對寫操做加鎖讀操做不加鎖。這樣一來,即最大化提高了併發訪問的能力,很是適合應用在讀多寫少的業務場景。這其實也是咱們在項目中,使用CopyOnWriteArrayList的一個主要應用場景。

2.2.3.CopyOnWriteArrayList源碼分析

經過前面兩個小結,咱們已經搞清楚CopyOnWriteArrayList的應用場景,並理解了什麼是寫時複製的思想。在你的項目中,根據業務須要,咱們在進行業務結構設計的時候,能夠借鑑寫時複製的這一思想,解決實際的業務問題。必定要學會活學活用,至於如何發揮,就留給你了。

接下來我帶你一塊兒看一下CopyOnWriteArrayList關鍵方法的源碼實現,進一步加深你對寫時複製思想的理解,咱們經過兩個主要的集合操做來看,分別是:

  • 添加集合元素(寫操做):add

/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
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;
      
     // 將新的集合,替換原集合
     setArray(newElements);
     return true;
  } finally {
    lock.unlock();
  }
}
  • 訪問集合元素(讀操做):get
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
   // 獲取集合中的元素,讀操做不須要加鎖
   return get(getArray(), index);
}

private E get(Object[] a, int index) {
        return (E) a[index];
}

經過add、get方法源碼,驗證了咱們前面分析的結論:寫操做加鎖、讀操做不須要加鎖

最後咱們以一個問答的形式結束本次分享,寫時複製思想適合應用在讀多寫少的業務場景下,最大化提高集合的併發訪問能力。咱們說:任何事物都有兩面性,你知道它的另外一面存在什麼侷限性嗎?

咱們直接給出答案,寫時複製思想的侷限性是:

  • 更加消耗空間資源,寫操做要從舊的集合,複製獲得一個新的集合,即新舊集合同時存在,更佔用內存資源

  • 另外寫操做加鎖,讀操做不加鎖的實現方式,會存在過時讀的問題

結合以上兩點,當你在項目中應用寫時複製思想進行業務架構設計的時候,或者使用CopyOnWriteArrayList的時候,必定要考慮業務上是否可以接受過時讀的問題。

相關文章
相關標籤/搜索