溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

高級(jí)并發(fā)編程系列之什么是CopyOnWriteArrayList

發(fā)布時(shí)間:2021-10-21 10:33:00 來(lái)源:億速云 閱讀:126 作者:iii 欄目:編程語(yǔ)言

本篇內(nèi)容介紹了“高級(jí)并發(fā)編程系列之什么是CopyOnWriteArrayList”的有關(guān)知識(shí),在實(shí)際案例的操作過(guò)程中,不少人都會(huì)遇到這樣的困境,接下來(lái)就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

1.考考你

在你看具體內(nèi)容前,讓我們一起先思考這么幾個(gè)問(wèn)題:

  • CopyOnWriteArrayList類(lèi)名稱(chēng)中,有我們熟悉的ArrayList,那么日常開(kāi)發(fā)使用ArrayList的時(shí)候,有什么你需要注意的地方嗎?

  • CopyOnWrite中文翻譯過(guò)來(lái),是寫(xiě)時(shí)復(fù)制,到底什么是寫(xiě)時(shí)復(fù)制呢?

  • 關(guān)于寫(xiě)時(shí)復(fù)制的思想,在什么場(chǎng)景下適合應(yīng)用,有什么需要注意的地方嗎?

帶著以上幾個(gè)問(wèn)題,讓我們一起開(kāi)始今天的內(nèi)容吧。

2.案例

2.1.ArrayList踩過(guò)的坑

2.1.1.同祖宗,不相忘

CopyOnWriteArrayList類(lèi)名稱(chēng)中,包含有ArrayList,這表明它們之間具有血緣關(guān)系,起源于一個(gè)老祖宗,我們先來(lái)看類(lèi)圖:

高級(jí)并發(fā)編程系列之什么是CopyOnWriteArrayList

2.1.2.ArrayList不能這么用

通過(guò)類(lèi)圖我們看到CopyOnWriteArrayList、ArrayList都實(shí)現(xiàn)了相同的接口。為了方便你更好的理解CopyOnWriteArrayList,我們先從ArrayList講起。

接下來(lái)我將通過(guò)日常開(kāi)發(fā)中使用ArrayList,我將給你分享需要有意識(shí)避開(kāi)的一些案例。

我們知道ArrayList底層是基于數(shù)組數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn),它的特性是:擁有數(shù)組的一切特性,且支持動(dòng)態(tài)擴(kuò)容。那么我們使用ArrayList,其實(shí)是把它作為容器來(lái)使用,對(duì)于容器,你能想到都有哪些常規(guī)操作嗎?

  • 將元素放入容器中

  • 更新容器中的某個(gè)元素

  • 刪除容器中的某個(gè)元素

  • 獲取容器中的某個(gè)元素

  • 循環(huán)遍歷容器中的元素

以上都是我們?cè)陧?xiàng)目中,使用容器時(shí)的一些高頻操作。對(duì)于每個(gè)操作,我就不帶著你一一演示了,你應(yīng)該都很熟悉。這里我們重點(diǎn)關(guān)注循環(huán)遍歷容器中的元素這個(gè)操作。

我們知道容器的循環(huán)遍歷操作,可以通過(guò)for循環(huán)遍歷,還可以通過(guò)迭代器循環(huán)遍歷。通過(guò)上面的類(lèi)圖,我們知道ArrayList頂層實(shí)現(xiàn)了Iterable接口,所以它是支持迭代器操作的,這里迭代器,即應(yīng)用了迭代器設(shè)計(jì)模式。關(guān)于設(shè)計(jì)模式的內(nèi)容,我們暫且不去深究,時(shí)間允許的話,我將在下一個(gè)系列與你分享我理解的面向?qū)ο缶幊?、設(shè)計(jì)原則、設(shè)計(jì)思想與設(shè)計(jì)模式。

接下來(lái)我通過(guò)ArrayList迭代器遍歷過(guò)程中,需要留意的一些地方。我們直接上代碼(show me the code):

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

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

/**
 * 演示ArrayList迭代器遍歷時(shí),需要注意的細(xì)節(jié)
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2020/12/26 10:50
 */
public class ShowMeArrayList {

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

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

    }

}

執(zhí)行結(jié)果:

當(dāng)前從容器中獲取的人是:zhangsan
當(dāng)前從容器中獲取的人是:lisi
當(dāng)前從容器中獲取的人是:wangwu

通過(guò)創(chuàng)建ArrayList實(shí)例,添加三個(gè)元素:zhangsan 、lisi、wangwu,并通過(guò)迭代器進(jìn)行遍歷輸出。這樣一來(lái)我們就準(zhǔn)備好了案例基礎(chǔ)案例代碼。

接下來(lái)我們做一些演化操作:

  • 在遍歷的過(guò)程中,通過(guò)ArrayList添加、或者刪除集合中的元素

  • 在遍歷的過(guò)程中,通過(guò)迭代器Iterator刪除集合中的元素

show me code:

/*
* 遍歷過(guò)程中,通過(guò)Iterator實(shí)例:刪除元素
* 預(yù)期結(jié)果:正常執(zhí)行
* */
Iterator<String> iter = list.iterator();
while(iter.hasNext()){
   // 如果當(dāng)前遍歷到lisi,我們將lisi從集合中刪除
   String name = iter.next();
   if("lisi".equals(name)){
        iter.remove();// 不會(huì)拋出異常   why?
    }
       System.out.println("當(dāng)前從容器中獲取的人是:"+ name);
}
System.out.println("刪除元素后,集合中還有元素:" + list);  

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

// 執(zhí)行結(jié)果
當(dāng)前從容器中獲取的人是:zhangsan
Exception in thread "main" java.util.ConcurrentModificationException
當(dāng)前從容器中獲取的人是: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.背后的邏輯

上面我們通過(guò)案例演示了ArrayList在迭代操作的時(shí)候,通過(guò)迭代器刪除元素操作,程序不會(huì)拋出異常;通過(guò)ArrayList添加、刪除,都會(huì)引起后續(xù)的迭代操作拋出異常。你知道這背后的邏輯嗎?

關(guān)于這個(gè)問(wèn)題,我從兩個(gè)角度給你分享:

  • 為什么迭代器操作中,不允許向原集合中添加、刪除元素?

  • ArrayList中,是如何控制迭代操作中,如何檢測(cè)原集合是否被添加、刪除操作過(guò)?

為了講清楚這個(gè)問(wèn)題,我們從圖開(kāi)始(一圖勝千言):

高級(jí)并發(fā)編程系列之什么是CopyOnWriteArrayList

高清楚為什么迭代器操作中,不允許向原集合中添加、刪除元素?這個(gè)問(wèn)題后,我們?cè)龠M(jìn)一步看ArrayList是如何檢測(cè)控制,在迭代過(guò)程中,原集合有添加、或者刪除操作這個(gè)問(wèn)題。

這里我將帶你看一下源代碼,這也是我建議你應(yīng)該要經(jīng)常做的事情,養(yǎng)成看源代碼習(xí)慣,我們常說(shuō):源碼之下無(wú)秘密。

/*
*ArrayList的迭代器,是一個(gè)內(nèi)部類(lèi)
*/
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator<E> {
    // 迭代器內(nèi)部游標(biāo),標(biāo)識(shí)下一個(gè)待遍歷元素的數(shù)組下標(biāo)
    int cursor;       // index of next element to return
    // 標(biāo)識(shí)已經(jīng)迭代的最后一個(gè)元素的數(shù)組下標(biāo)
    int lastRet = -1; // index of last element returned; -1 if no such
    
    // 注意:這個(gè)變量很重要,它是整個(gè)迭代器迭代過(guò)程中
    // 標(biāo)識(shí)原集合被添加、刪除操作的次數(shù)
    // 初始值是集合中的成員變量:modCount(集合被添加、刪除操作計(jì)數(shù)值)
    int expectedModCount = modCount;

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

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

/*
*迭代器 next方法
*/
public E next() {
  // 關(guān)鍵代碼:檢查原集合是否被添加、或者刪除操作
  // 如果有添加,或者刪除操作,那么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();
}

通過(guò)ArrayList內(nèi)部類(lèi)迭代器Itr的源碼分析,我們看到迭代器的源碼實(shí)現(xiàn)非常簡(jiǎn)答,并且恭喜你!在不知覺(jué)中你還學(xué)會(huì)了迭代器設(shè)計(jì)模式的實(shí)現(xiàn)。

最后我們?cè)偻ㄟ^(guò)查看ArrayList中add、remove方法的源碼,解惑modCount成員變量的問(wèn)題:

/*
*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) {
  // 注釋說(shuō)了:會(huì)將modCount成員變量加1 
  //繼續(xù)看ensureCapacityInternal方法
  ensureCapacityInternal(size + 1);  // Increments modCount!!
  elementData[size++] = e;
  return true;
}

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

  // overflow-conscious code
  if (minCapacity - elementData.length > 0)
      // 擴(kuò)容操作,留給你去看了
      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;
}

通過(guò)圖、和源碼分析的方式,現(xiàn)在你應(yīng)該可以更好的理解ArrayList、和它的內(nèi)部迭代器Itr,并且在你的項(xiàng)目中可以很好的使用ArrayList。

這也是我重點(diǎn)想要分享給你的地方:持續(xù)學(xué)習(xí),做到知其然,且知其所以然,一種專(zhuān)研的精神。年輕人少刷點(diǎn)抖音、快手、少看點(diǎn)直播,這些東西除了消耗掉你的精氣神外,不會(huì)給你帶來(lái)任何正向價(jià)值的東西。

2.2.CopyOnWriteArrayList詳解

2.2.1.CopyOnWriteArrayList初體驗(yàn)

為了方便你理解CopyOnWriteArrayList,我煞費(fèi)苦心的帶你一路分析ArrayList?,F(xiàn)在讓我們先直觀的看一下CopyOnWriteArrayList。還是通過(guò)前面的案例,即迭代器迭代過(guò)程中,給原集合添加,或者刪除元素。

我們通過(guò)ArrayList演示案例的時(shí)候,你還記得吧,會(huì)拋出異常,至于異常的原因在前面的內(nèi)容中,我?guī)阋黄鹱隽藢?zhuān)門(mén)的分析。如果你不記得了,建議回頭再去看一看。

現(xiàn)在我重點(diǎn)通過(guò)CopyOnWriteArrayList來(lái)演示案例,看在相同的場(chǎng)景下,是否還會(huì)拋出異常?你需要重點(diǎn)關(guān)心一下這個(gè)地方。

show me the code:

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

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

/**
 * 演示CopyOnWriteArrayList迭代器遍歷時(shí),需要注意的細(xì)節(jié)
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2020/12/26 10:50
 */
public class ShowMeCopyOnWriteArrayList {

   public static void main(String[] args) {
     // 創(chuàng)建一個(gè)CopyOnWriteArrayList
     CopyOnWriteArrayList<String> list =new CopyOnWriteArrayList<>();

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

     /*
     * 遍歷過(guò)程中,通過(guò)CopyOnWriteArrayList實(shí)例:添加、或者刪除元素
     * 預(yù)期結(jié)果:正常執(zhí)行
     * */
     Iterator<String> iter = list.iterator();
     while(iter.hasNext()){
        // 如果當(dāng)前遍歷到lisi,我們向集合中添加:小明
        String name = iter.next();
        if("lisi".equals(name)){
           list.add("小明");// 不會(huì)拋出異常   why?
        }
        System.out.println("當(dāng)前從容器中獲取的人是:"+ name);
     }
     System.out.println("添加元素后,集合中還有元素:" + list);

   }

}

執(zhí)行結(jié)果:

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

通過(guò)執(zhí)行結(jié)果看到,使用CopyOnWriteArrayList,在迭代器迭代過(guò)程中,向原集合中添加了一個(gè)新的元素:小明。迭代器繼續(xù)迭代并不會(huì)拋出異常,且最后打印結(jié)果顯示小明確認(rèn)已經(jīng)添加到了集合中

對(duì)于這個(gè)結(jié)果,你是不是感到多少有點(diǎn)意外!感覺(jué)與ArrayList不是一個(gè)套路對(duì)吧。它到底是如何實(shí)現(xiàn)的呢?

2.2.2.寫(xiě)時(shí)復(fù)制思想

剛才我們通過(guò)CopyOnWriteArrayList,與ArrayList做了案例演示的對(duì)比,發(fā)現(xiàn)它們?cè)趫?zhí)行結(jié)果上有很大的不一樣。結(jié)果差異的本質(zhì)原因是CopyOnWriteArrayList類(lèi)名稱(chēng)中的關(guān)鍵字:CopyOnWrite,中文翻譯過(guò)來(lái)是:寫(xiě)時(shí)復(fù)制。

到底什么是寫(xiě)時(shí)復(fù)制呢?所謂寫(xiě)時(shí)復(fù)制,它直觀的含義是:

  • 我已經(jīng)有了一個(gè)集合A,當(dāng)需要往集合A中添加一個(gè)元素,或者刪除一個(gè)元素的時(shí)候

  • 保持A集合不變,從A集合復(fù)制一個(gè)新的集合B

  • 對(duì)應(yīng)向新集合B中添加、或者刪除元素,操作完畢后,將A指向新的B集合,即用新的集合,替換舊的集合

     

你看這就是寫(xiě)時(shí)復(fù)制的思想,理解起來(lái)并不困難。這樣做有什么好處呢?好處就是當(dāng)我們通過(guò)迭代器訪問(wèn)集合的時(shí)候,我們可以同時(shí)允許向集合中添加、刪除集合元素,有效避免了訪問(wèn)集合(讀操作),與更新集合(寫(xiě)操作)的沖突,最大化實(shí)現(xiàn)了集合的并發(fā)訪問(wèn)性能。

那么關(guān)于CopyOnWriteArrayList,它是如何最大化提升并發(fā)訪問(wèn)能力呢?它的實(shí)現(xiàn)原理并不復(fù)雜,既然是并發(fā)訪問(wèn),線程安全的問(wèn)題不可回避,你應(yīng)該也想到了,首先加鎖是必須的。

除了加鎖,還需要考慮提升并發(fā)訪問(wèn)的能力,如何提升?實(shí)現(xiàn)也很簡(jiǎn)單,針對(duì)寫(xiě)操作加鎖,讀操作不加鎖。這樣一來(lái),即最大化提升了并發(fā)訪問(wèn)的能力,非常適合應(yīng)用在讀多寫(xiě)少的業(yè)務(wù)場(chǎng)景。這其實(shí)也是我們?cè)陧?xiàng)目中,使用CopyOnWriteArrayList的一個(gè)主要應(yīng)用場(chǎng)景。

2.2.3.CopyOnWriteArrayList源碼分析

通過(guò)前面兩個(gè)小結(jié),我們已經(jīng)搞清楚CopyOnWriteArrayList的應(yīng)用場(chǎng)景,并理解了什么是寫(xiě)時(shí)復(fù)制的思想。在你的項(xiàng)目中,根據(jù)業(yè)務(wù)需要,我們?cè)谶M(jìn)行業(yè)務(wù)結(jié)構(gòu)設(shè)計(jì)的時(shí)候,可以借鑒寫(xiě)時(shí)復(fù)制的這一思想,解決實(shí)際的業(yè)務(wù)問(wèn)題。一定要學(xué)會(huì)活學(xué)活用,至于如何發(fā)揮,就留給你了。

接下來(lái)我?guī)阋黄鹂匆幌翪opyOnWriteArrayList關(guān)鍵方法的源碼實(shí)現(xiàn),進(jìn)一步加深你對(duì)寫(xiě)時(shí)復(fù)制思想的理解,我們通過(guò)兩個(gè)主要的集合操作來(lái)看,分別是:

  • 添加集合元素(寫(xiě)操作):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) {
  // 寫(xiě)操作,需要加鎖
  final ReentrantLock lock = this.lock;
  lock.lock();
  try {
     // 復(fù)制原集合,且將新元素添加到復(fù)制集合中
     Object[] elements = getArray();
     int len = elements.length;
     Object[] newElements = Arrays.copyOf(elements, len + 1);
     newElements[len] = e;
      
     // 將新的集合,替換原集合
     setArray(newElements);
     return true;
  } finally {
    lock.unlock();
  }
}
  • 訪問(wèn)集合元素(讀操作):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];
}

通過(guò)add、get方法源碼,驗(yàn)證了我們前面分析的結(jié)論:寫(xiě)操作加鎖、讀操作不需要加鎖。

最后我們以一個(gè)問(wèn)答的形式結(jié)束本次分享,寫(xiě)時(shí)復(fù)制思想適合應(yīng)用在讀多寫(xiě)少的業(yè)務(wù)場(chǎng)景下,最大化提升集合的并發(fā)訪問(wèn)能力。我們說(shuō):任何事物都有兩面性,你知道它的另一面存在什么局限性嗎?

我們直接給出答案,寫(xiě)時(shí)復(fù)制思想的局限性是:

  • 更加消耗空間資源,寫(xiě)操作要從舊的集合,復(fù)制得到一個(gè)新的集合,即新舊集合同時(shí)存在,更占用內(nèi)存資源

  • 另外寫(xiě)操作加鎖,讀操作不加鎖的實(shí)現(xiàn)方式,會(huì)存在過(guò)期讀的問(wèn)題

結(jié)合以上兩點(diǎn),當(dāng)你在項(xiàng)目中應(yīng)用寫(xiě)時(shí)復(fù)制思想進(jìn)行業(yè)務(wù)架構(gòu)設(shè)計(jì)的時(shí)候,或者使用CopyOnWriteArrayList的時(shí)候,一定要考慮業(yè)務(wù)上是否能夠接受過(guò)期讀的問(wèn)題。

“高級(jí)并發(fā)編程系列之什么是CopyOnWriteArrayList”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI