java 并發(fā)神器 - CopyOnWriteArrayList

相信大家對(duì) ConcurrentHashMap 這個(gè)線程安全類非常熟悉,但是如果我想在多線程環(huán)境下使用 ArrayList,該怎么處理呢?阿粉今天來(lái)給你揭曉答案!

一、摘要

在介紹 CopyOnWriteArrayList 之前,我們一起先來(lái)看看如下方法執(zhí)行結(jié)果,代碼內(nèi)容如下:

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("1");
    list.add("2");
    list.add("1");
    System.out.println("原始list元素:"+ list.toString());
    //通過(guò)對(duì)象移除等于內(nèi)容為1的元素
    for (String item : list) {
        if("1".equals(item)) {
            list.remove(item);
        }
    }
    System.out.println("通過(guò)對(duì)象移除后的list元素:"+ list.toString());
}

執(zhí)行結(jié)果內(nèi)容如下:

原始list元素:[1, 2, 1]
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
    at java.util.ArrayList$Itr.next(ArrayList.java:859)
    at com.example.container.a.TestList.main(TestList.java:16)

很遺憾,結(jié)果并沒(méi)有達(dá)到我們想要的預(yù)期效果,執(zhí)行之后直接報(bào)錯(cuò)!拋ConcurrentModificationException異常!

為啥會(huì)拋這個(gè)異常呢?

我們一起來(lái)看看,foreach 寫法實(shí)際上是對(duì)List.iterator() 迭代器的一種簡(jiǎn)寫,因此我們可以從分析List.iterator() 迭代器進(jìn)行入手,看看為啥會(huì)拋這個(gè)異常。

ArrayList類中的Iterator迭代器實(shí)現(xiàn),源碼內(nèi)容:

通過(guò)代碼我們發(fā)現(xiàn) Itr 是 ArrayList 中定義的一個(gè)私有內(nèi)部類,每次調(diào)用next、remove方法時(shí),都會(huì)調(diào)用checkForComodification方法,源碼如下:

/**修改次數(shù)檢查*/
final void checkForComodification() {
    //檢查L(zhǎng)ist中的修改次數(shù)是否與迭代器類中的修改次數(shù)相等
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

checkForComodification方法,實(shí)際上是用來(lái)檢查L(zhǎng)ist中的修改次數(shù)modCount是否與迭代器類中的修改次數(shù)expectedModCount相等,如果不相等,就會(huì)拋出ConcurrentModificationException異常!

那么問(wèn)題基本上已經(jīng)清晰了,上面的運(yùn)行結(jié)果之所以會(huì)拋出這個(gè)異常,就是因?yàn)長(zhǎng)ist中的修改次數(shù)modCount與迭代器類中的修改次數(shù)expectedModCount不相同造成的!

閱讀過(guò)集合源碼的朋友,可能想起Vector這個(gè)類,它不是 JDK 中 ArrayList 線程安全的一個(gè)版本么?

好的,為了眼見(jiàn)為實(shí),我們把ArrayList換成Vector來(lái)測(cè)試一下,代碼如下:

public static void main(String[] args) {
    Vector<String> list = new Vector<String>();
    //模擬10個(gè)線程向list中添加內(nèi)容,并且讀取內(nèi)容
    for (int i = 0; i < 5; i++) {
        final int j = i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                //添加內(nèi)容
                list.add(j + "-j");

                //讀取內(nèi)容
                for (String str : list) {
                    System.out.println("內(nèi)容:" + str);
                }
            }
        }).start();
    }
}

執(zhí)行程序,運(yùn)行結(jié)果如下:


還是一樣的結(jié)果,拋異常了,Vector雖然線程安全,只不過(guò)是加了synchronized關(guān)鍵字,但是迭代問(wèn)題完全沒(méi)有解決!

繼續(xù)回到本文要介紹的 CopyOnWriteArrayList 類,我們把上面的例子,換成CopyOnWriteArrayList類來(lái)試試,源碼內(nèi)容如下:

public static void main(String[] args) {
    //將ArrayList換成CopyOnWriteArrayList
    CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    list.add("1");
    list.add("2");
    list.add("1");
    System.out.println("原始list元素:"+ list.toString());

    //通過(guò)對(duì)象移除等于11的元素
    for (String item : list) {
        if("1".equals(item)) {
            list.remove(item);
        }
    }
    System.out.println("通過(guò)對(duì)象移除后的list元素:"+ list.toString());
}

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

原始list元素:[1, 2, 1]
通過(guò)對(duì)象移除后的list元素:[2]

呃呵,執(zhí)行成功了,沒(méi)有報(bào)錯(cuò)!是不是很神奇~~

當(dāng)然,類似上面這樣的例子有很多,比如寫10個(gè)線程向list中添加元素讀取內(nèi)容,也會(huì)拋出上面那個(gè)異常,操作如下:

public static void main(String[] args) {
    final List<String> list = new ArrayList<>();
    //模擬10個(gè)線程向list中添加內(nèi)容,并且讀取內(nèi)容
    for (int i = 0; i < 10; i++) {
        final int j = i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                //添加內(nèi)容
                list.add(j + "-j");

                //讀取內(nèi)容
                for (String str : list) {
                    System.out.println("內(nèi)容:" + str);
                }
            }
        }).start();
    }
}

類似的操作例子就非常多了,這里就不一一舉例了。

CopyOnWriteArrayList 實(shí)際上是 ArrayList 一個(gè)線程安全的操作類!

從它的名字可以看出,CopyOnWrite 是在寫入的時(shí)候,不修改原內(nèi)容,而是將原來(lái)的內(nèi)容復(fù)制一份到新的數(shù)組,然后向新數(shù)組寫完數(shù)據(jù)之后,再移動(dòng)內(nèi)存指針,將目標(biāo)指向最新的位置。

二、簡(jiǎn)介

從 JDK1.5 開(kāi)始 Java 并發(fā)包里提供了兩個(gè)使用CopyOnWrite 機(jī)制實(shí)現(xiàn)的并發(fā)容器,分別是CopyOnWriteArrayList和CopyOnWriteArraySet 。

從名字上看,CopyOnWriteArrayList主要針對(duì)動(dòng)態(tài)數(shù)組,一個(gè)線程安全版本的 ArrayList !

而CopyOnWriteArraySet主要針對(duì)集,CopyOnWriteArraySet可以理解為HashSet線程安全的操作類,我們都知道HashSet基于散列表HashMap實(shí)現(xiàn),但是CopyOnWriteArraySet并不是基于散列表實(shí)現(xiàn),而是基于CopyOnWriteArrayList動(dòng)態(tài)數(shù)組實(shí)現(xiàn)!

關(guān)于這一點(diǎn),我們可以從它的源碼中得出結(jié)論,部分源碼內(nèi)容:


從源碼上可以看出,CopyOnWriteArraySet默認(rèn)初始化的時(shí)候,實(shí)例化了CopyOnWriteArrayList類,CopyOnWriteArraySet的大部分方法,例如add、remove等方法都基于CopyOnWriteArraySet實(shí)現(xiàn)!

兩者最大的不同點(diǎn)是,CopyOnWriteArrayList可以允許元素重復(fù),而CopyOnWriteArraySet不允許有重復(fù)的元素!

好了,繼續(xù)來(lái) BB 本文要介紹的CopyOnWriteArrayList類~~

打開(kāi)CopyOnWriteArrayList類的源碼,內(nèi)容如下:

可以看到 CopyOnWriteArrayList 的存儲(chǔ)元素的數(shù)組array變量,使用了volatile關(guān)鍵字保證的多線程下數(shù)據(jù)可見(jiàn)行;同時(shí),使用了ReentrantLock可重入鎖對(duì)象,保證線程操作安全。

在初始化階段,CopyOnWriteArrayList默認(rèn)給數(shù)組初始化了一個(gè)對(duì)象,當(dāng)然,初始化方法還有很多,比如如下我們經(jīng)常會(huì)用到的一個(gè)初始化方法,源碼內(nèi)容如下:


這個(gè)方法,表示如果我們傳入的是一個(gè) ArrayList數(shù)組對(duì)象,會(huì)將對(duì)象內(nèi)容復(fù)制一份到新的數(shù)組中,然后初始化進(jìn)去,操作如下:

List<String> list = new ArrayList<>();
...
//CopyOnWriteArrayList將list內(nèi)容復(fù)制出來(lái),并創(chuàng)建一個(gè)新的數(shù)組
CopyOnWriteArrayList<String> copyList = new CopyOnWriteArrayList<>(list);

CopyOnWriteArrayList是對(duì)原數(shù)組內(nèi)容進(jìn)行復(fù)制再寫入,那么是不是也存在多線程下操作也會(huì)發(fā)生沖突呢?

下面我們?cè)僖黄饋?lái)看看它的方法實(shí)現(xiàn)!

三、常用方法

3.1、添加元素

add()方法是CopyOnWriteArrayList的添加元素的入口!

CopyOnWriteArrayList之所以能保證多線程下安全操作, add()方法功不可沒(méi),源碼如下:


操作步驟如下:

1、獲得對(duì)象鎖;
2、獲取數(shù)組內(nèi)容;
3、將原數(shù)組內(nèi)容復(fù)制到新數(shù)組;
4、寫入數(shù)據(jù);
5、將array數(shù)組變量地址指向新數(shù)組;
6、釋放對(duì)象鎖;
在 Java 中,獨(dú)占鎖方面,有2種方式可以保證線程操作安全,一種是使用虛擬機(jī)提供的synchronized 來(lái)保證并發(fā)安全,另一種是使用JUC包下的ReentrantLock可重入鎖來(lái)保證線程操作安全。

CopyOnWriteArrayList使用了ReentrantLock這種可重入鎖,保證了線程操作安全,同時(shí)數(shù)組變量array使用volatile保證多線程下數(shù)據(jù)的可見(jiàn)行!

其他的,還有指定下標(biāo)進(jìn)行添加的方法,如add(int index, E element),操作類似,先找到需要添加的位置,如果是中間位置,則以添加位置為分界點(diǎn),分兩次進(jìn)行復(fù)制,最后寫入數(shù)據(jù)!

3.2、移除元素

remove()方法是CopyOnWriteArrayList的移除元素的入口!

源碼如下:


操作類似添加方法,步驟如下:

1、獲得對(duì)象鎖;
2、獲取數(shù)組內(nèi)容;
3、判斷移除的元素是否為數(shù)組最后的元素,如果是最后的元素,直接將舊元素內(nèi)容復(fù)制到新數(shù)組,并重新設(shè)置array值;
4、如果是中間元素,以index為分界點(diǎn),分兩節(jié)復(fù)制;
5、將array數(shù)組變量地址指向新數(shù)組;
6、釋放對(duì)象鎖;
當(dāng)然,移除的方法還有基于對(duì)象的remove(Object o),原理也是一樣的,先找到元素的下標(biāo),然后執(zhí)行移除操作。

3.3、查詢?cè)?/h4>

get()方法是CopyOnWriteArrayList的查詢?cè)氐娜肟冢?/p>

源碼如下:

public E get(int index) {
    //獲取數(shù)組內(nèi)容,通過(guò)下標(biāo)直接獲取
    return get(getArray(), index);
}

查詢因?yàn)椴簧婕暗綌?shù)據(jù)操作,所以無(wú)需使用鎖進(jìn)行處理!

3.4、遍歷元素

上文中我們介紹到,基本都是在遍歷元素的時(shí)候因?yàn)樾薷拇螖?shù)與迭代器中的修改次數(shù)不一致,導(dǎo)致檢查的時(shí)候拋異常,我們一起來(lái)看看CopyOnWriteArrayList迭代器實(shí)現(xiàn)。

打開(kāi)源碼,可以得出CopyOnWriteArrayList返回的迭代器是COWIterator,源碼如下:

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

打開(kāi)COWIterator類,其實(shí)它是CopyOnWriteArrayList的一個(gè)靜態(tài)內(nèi)部類,源碼如下:


可以看出,在使用迭代器的時(shí)候,遍歷的元素都來(lái)自于上面的getArray()方法傳入的對(duì)象數(shù)組,也就是傳遞進(jìn)來(lái)的 array 數(shù)組!

由此可見(jiàn),CopyOnWriteArrayList 在使用迭代器遍歷的時(shí)候,操作的都是原數(shù)組,沒(méi)有像上面那樣進(jìn)行修改次數(shù)判斷,所以不會(huì)拋異常!

當(dāng)然,從源碼上也可以得出,使用CopyOnWriteArrayList的迭代器進(jìn)行遍歷元素的時(shí)候,不能調(diào)用remove()方法移除元素,因?yàn)椴恢С执瞬僮鳎?/p>

如果想要移除元素,只能使用CopyOnWriteArrayList提供的remove()方法,而不是迭代器的remove()方法,這個(gè)需要注意一下!

四、總結(jié)

CopyOnWriteArrayList是一個(gè)典型的讀寫分離的動(dòng)態(tài)數(shù)組操作類!

在寫入數(shù)據(jù)的時(shí)候,將舊數(shù)組內(nèi)容復(fù)制一份出來(lái),然后向新的數(shù)組寫入數(shù)據(jù),最后將新的數(shù)組內(nèi)存地址返回給數(shù)組變量;移除操作也類似,只是方式是移除元素而不是添加元素;而查詢方法,因?yàn)椴簧婕熬€程操作,所以并沒(méi)有加鎖出來(lái)!

因?yàn)镃opyOnWriteArrayList讀取內(nèi)容沒(méi)有加鎖,在寫入數(shù)據(jù)的時(shí)候同時(shí)也可以進(jìn)行讀取數(shù)據(jù)操作,因此性能得到很大的提升,但是也有缺陷,對(duì)于邊讀邊寫的情況,不一定能實(shí)時(shí)的讀到最新的數(shù)據(jù),比如如下操作:

public static void main(String[] args) throws InterruptedException {
    final CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    list.add("a");
    list.add("b");
    for (int i = 0; i < 5; i++) {
        final int j =i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                //寫入數(shù)據(jù)
                list.add("i-" + j);
                //讀取數(shù)據(jù)
                for (String str : list) {
                    System.out.println("線程-" + Thread.currentThread().getName() + ",讀取內(nèi)容:" + str);
                }
            }
        }).start();
    }
}

新建5個(gè)線程向list中添加元素,執(zhí)行結(jié)果如下:


可以看到,5個(gè)線程的讀取內(nèi)容有差異!

因此CopyOnWriteArrayList很適合讀多寫少的應(yīng)用場(chǎng)景!

引用鏈接:http://www.justdojava.com/2020/01/17/java-collection-16/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容