最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java中的CopyOnWriteArrayList深入解讀

 更新時間:2023年12月18日 11:47:33   作者:老猿說說  
這篇文章主要介紹了Java中的CopyOnWriteArrayList深入解讀,在 ArrayList 的類注釋上,JDK 就提醒了我們,如果要把 ArrayList 作為共享變量的話,是線程不安全的,需要的朋友可以參考下

引導(dǎo)語

在 ArrayList 的類注釋上,JDK 就提醒了我們,如果要把 ArrayList 作為共享變量的話,是線程不安全的,推薦我們自己加鎖或者使用 Collections.synchronizedList 方法,其實(shí) JDK 還提供了另外一種線程安全的 List,叫做 CopyOnWriteArrayList,這個 List 具有以下特征:

線程安全的,多線程環(huán)境下可以直接使用,無需加鎖; 通過鎖 + 數(shù)組拷貝 + volatile 關(guān)鍵字保證了線程安全; 每次數(shù)組操作,都會把數(shù)組拷貝一份出來,在新數(shù)組上進(jìn)行操作,操作成功之后再賦值回去。

1 整體架構(gòu)

從整體架構(gòu)上來說,CopyOnWriteArrayList 數(shù)據(jù)結(jié)構(gòu)和 ArrayList 是一致的,底層是個數(shù)組,只不過 CopyOnWriteArrayList 在對數(shù)組進(jìn)行操作的時候,基本會分四步走:

  1. 加鎖;
  2. 從原數(shù)組中拷貝出新數(shù)組;
  3. 在新數(shù)組上進(jìn)行操作,并把新數(shù)組賦值給數(shù)組容器;
  4. 解鎖

除了加鎖之外,CopyOnWriteArrayList 的底層數(shù)組還被 volatile 關(guān)鍵字修飾,意思是一旦數(shù)組被修改,其它線程立馬能夠感知到,代碼如下:

private transient volatile Object[] array;

整體上來說,CopyOnWriteArrayList 就是利用鎖 + 數(shù)組拷貝 + volatile 關(guān)鍵字保證了 List 的線程安全。

1.1 類注釋

我們看看從 CopyOnWriteArrayList 的類注釋上能得到哪些信息:

  1. 所有的操作都是線程安全的,因為操作都是在新拷貝數(shù)組上進(jìn)行的;
  2. 數(shù)組的拷貝雖然有一定的成本,但往往比一般的替代方案效率高;
  3. 迭代過程中,不會影響到原來的數(shù)組,也不會拋出 ConcurrentModificationException 異常。

接著我們來看下 CopyOnWriteArrayList 的核心方法源碼。

2 新增

新增有很多種情況,比如說:新增到數(shù)組尾部、新增到數(shù)組某一個索引位置、批量新增等等,操作的思路還是我們開頭說的四步,我們拿新增到數(shù)組尾部的方法舉例,來看看底層源碼的實(shí)現(xiàn):

// 添加元素到數(shù)組尾部
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // 加鎖
    lock.lock();
    try {
        // 得到所有的原數(shù)組
        Object[] elements = getArray();
        int len = elements.length;
        // 拷貝到新數(shù)組里面,新數(shù)組的長度是 + 1 的,因為新增會多一個元素
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 在新數(shù)組中進(jìn)行賦值,新元素直接放在數(shù)組的尾部
        newElements[len] = e;
        // 替換掉原來的數(shù)組
        setArray(newElements);
        return true;
    // finally 里面釋放鎖,保證即使 try 發(fā)生了異常,仍然能夠釋放鎖   
    } finally {
        lock.unlock();
    }
}

從源碼中,我們發(fā)現(xiàn)整個 add 過程都是在持有鎖的狀態(tài)下進(jìn)行的,通過加鎖,來保證同一時刻只能有一個線程能夠?qū)ν粋€數(shù)組進(jìn)行 add 操作。

除了加鎖之外,還會從老數(shù)組中創(chuàng)建出一個新數(shù)組,然后把老數(shù)組的值拷貝到新數(shù)組上,這時候就有一個問題:都已經(jīng)加鎖了,為什么需要拷貝數(shù)組,而不是在原來數(shù)組上面進(jìn)行操作呢,原因主要為:

  • volatile 關(guān)鍵字修飾的是數(shù)組,如果我們簡單的在原來數(shù)組上修改其中某幾個元素的值,是無法觸發(fā)可見性的,我們必須通過修改數(shù)組的內(nèi)存地址才行,也就說要對數(shù)組進(jìn)行重新賦值才行。
  • 在新的數(shù)組上進(jìn)行拷貝,對老數(shù)組沒有任何影響,只有新數(shù)組完全拷貝完成之后,外部才能訪問到,降低了在賦值過程中,老數(shù)組數(shù)據(jù)變動的影響。

簡單 add 操作是直接添加到數(shù)組的尾部,接著我們來看下指定位置添加元素的關(guān)鍵源碼(部分源碼):

// len:數(shù)組的長度、index:插入的位置
int numMoved = len - index;
// 如果要插入的位置正好等于數(shù)組的末尾,直接拷貝數(shù)組即可
if (numMoved == 0)
    newElements = Arrays.copyOf(elements, len + 1);
else {
// 如果要插入的位置在數(shù)組的中間,就需要拷貝 2 次
// 第一次從 0 拷貝到 index。
// 第二次從 index+1 拷貝到末尾。
    newElements = new Object[len + 1];
    System.arraycopy(elements, 0, newElements, 0, index);
    System.arraycopy(elements, index, newElements, index + 1,
         numMoved);
}
// index 索引位置的值是空的,直接賦值即可。
newElements[index] = element;
// 把新數(shù)組的值賦值給數(shù)組的容器中
setArray(newElements);

從源碼中可以看到,當(dāng)插入的位置正好處于末尾時,只需要拷貝一次,當(dāng)插入的位置處于中間時,此時我們會把原數(shù)組一分為二,進(jìn)行兩次拷貝操作。

最后還有個批量新增操作,源碼我們就不貼了,底層也是拷貝數(shù)組的操作。

2.1 小結(jié)

從 add 系列方法可以看出,CopyOnWriteArrayList 通過加鎖 + 數(shù)組拷貝+ volatile 來保證了線程安全,每一個要素都有著其獨(dú)特的含義:

  1. 加鎖:保證同一時刻數(shù)組只能被一個線程操作;
  2. 數(shù)組拷貝:保證數(shù)組的內(nèi)存地址被修改,修改后觸發(fā) volatile 的可見性,其它線程可以立馬知道數(shù)組已經(jīng)被修改;
  3. volatile:值被修改后,其它線程能夠立馬感知最新值。

3 個要素缺一不可,比如說我們只使用 1 和 3 ,去掉 2,這樣當(dāng)我們修改數(shù)組中某個值時,并不會觸發(fā) volatile 的可見特性的,只有當(dāng)數(shù)組內(nèi)存地址被修改后,才能觸發(fā)把最新值通知給其他線程的特性。

3 刪除

接著我們來看下指定數(shù)組索引位置刪除的源碼:

// 刪除某個索引位置的數(shù)據(jù)
public E remove(int index) {
    final ReentrantLock lock = this.lock;
    // 加鎖
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        // 先得到老值
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        // 如果要刪除的數(shù)據(jù)正好是數(shù)組的尾部,直接刪除
        if (numMoved == 0)
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            // 如果刪除的數(shù)據(jù)在數(shù)組的中間,分三步走
            // 1. 設(shè)置新數(shù)組的長度減一,因為是減少一個元素
            // 2. 從 0 拷貝到數(shù)組新位置
            // 3. 從新位置拷貝到數(shù)組尾部
            Object[] newElements = new Object[len - 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            setArray(newElements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

步驟分為三步:

  1. 加鎖;
  2. 判斷刪除索引的位置,從而進(jìn)行不同策略的拷貝;
  3. 解鎖。

代碼整體的結(jié)構(gòu)風(fēng)格也比較統(tǒng)一:鎖 + try finally +數(shù)組拷貝,鎖被 final 修飾的,保證了在加鎖過程中,鎖的內(nèi)存地址肯定不會被修改,finally 保證鎖一定能夠被釋放,數(shù)組拷貝是為了刪除其中某個位置的元素。

4 批量刪除

數(shù)組的批量刪除很有意思,接下來我們來看下 CopyOnWriteArrayList 的批量刪除的實(shí)現(xiàn)過程:

// 批量刪除包含在 c 中的元素
public boolean removeAll(Collection<?> c) {
    if (c == null) throw new NullPointerException();
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        // 說明數(shù)組有值,數(shù)組無值直接返回 false
        if (len != 0) {
            // newlen 表示新數(shù)組的索引位置,新數(shù)組中存在不包含在 c 中的元素
            int newlen = 0;
            Object[] temp = new Object[len];
            // 循環(huán),把不包含在 c 里面的元素,放到新數(shù)組中
            for (int i = 0;
 i < len;
 ++i) {
                Object element = elements[i];
                // 不包含在 c 中的元素,從 0 開始放到新數(shù)組中
                if (!c.contains(element))
                    temp[newlen++] = element;
            }
            // 拷貝新數(shù)組,變相的刪除了不包含在 c 中的元素
            if (newlen != len) {
                setArray(Arrays.copyOf(temp, newlen));
                return true;
            }
        }
        return false;
    } finally {
        lock.unlock();
    }
}

從源碼中,我們可以看到,我們并不會直接對數(shù)組中的元素進(jìn)行挨個刪除,而是先對數(shù)組中的值進(jìn)行循環(huán)判斷,把我們不需要刪除的數(shù)據(jù)放到臨時數(shù)組中,最后臨時數(shù)組中的數(shù)據(jù)就是我們不需要刪除的數(shù)據(jù)。

不知道大家有木有似曾相識的感覺,ArrayList 的批量刪除的思想也是和這個類似的,所以我們在需要刪除多個元素的時候,最好都使用這種批量刪除的思想,而不是采用在 for 循環(huán)中使用單個刪除的方法,單個刪除的話,在每次刪除的時候都會進(jìn)行一次數(shù)組拷貝(刪除最后一個元素時不會拷貝),很消耗性能,也耗時,會導(dǎo)致加鎖時間太長,并發(fā)大的情況下,會造成大量請求在等待鎖,這也會占用一定的內(nèi)存。

5 其它方法

5.1 indexOf

indexOf 方法的主要用處是查找元素在數(shù)組中的下標(biāo)位置,如果元素存在就返回元素的下標(biāo)位置,元素不存在的話返回 -1,不但支持 null 值的搜索,還支持正向和反向的查找,我們以正向查找為例,通過源碼來說明一下其底層的實(shí)現(xiàn)方式:

// o:我們需要搜索的元素
// elements:我們搜索的目標(biāo)數(shù)組
// index:搜索的開始位置
// fence:搜索的結(jié)束位置
private static int indexOf(Object o, Object[] elements,
                           int index, int fence) {
    // 支持對 null 的搜索
    if (o == null) {
        for (int i = index;i < fence;i++)
            // 找到第一個 null 值,返回下標(biāo)索引的位置
            if (elements[i] == null)
                return i;
    } else {
        // 通過 equals 方法來判斷元素是否相等
        // 如果相等,返回元素的下標(biāo)位置
        for (int i = index;i < fence;i++)
            if (o.equals(elements[i]))
                return i;
    }
    return -1;
}

indexOf 方法在 CopyOnWriteArrayList 內(nèi)部使用也比較廣泛,比如在判斷元素是否存在時(contains),在刪除元素方法中校驗元素是否存在時,都會使用到 indexOf 方法,indexOf 方法通過一次 for 循環(huán)來查找元素,我們在調(diào)用此方法時,需要注意如果找不到元素時,返回的是 -1,所以有可能我們會對這個特殊值進(jìn)行判斷。

5.2 迭代

在 CopyOnWriteArrayList 類注釋中,明確說明了,在其迭代過程中,即使數(shù)組的原值被改變,也不會拋出 ConcurrentModificationException 異常,其根源在于數(shù)組的每次變動,都會生成新的數(shù)組,不會影響老數(shù)組,這樣的話,迭代過程中,根本就不會發(fā)生迭代數(shù)組的變動,我們截幾個圖說明一下:

迭代是直接持有原有數(shù)組的引用,也就是說迭代過程中,一旦原有數(shù)組的值內(nèi)存地址發(fā)生變化,必然會影響到迭代過程,下圖源碼演示的是 CopyOnWriteArrayList 的迭代方法,我們可以看到迭代器是直接持有原數(shù)組的引用:

link

我們寫了一個 demo,在 CopyOnWriteArrayList 迭代之后,往 CopyOnWriteArrayList 里面新增值,從下圖中可以看到在 CopyOnWriteArrayList 迭代之前,數(shù)組的內(nèi)存地址是 962,請記住這個數(shù)字:

link

CopyOnWriteArrayList 迭代之后,我們使用 add(“50”) 代碼給數(shù)組新增一個數(shù)據(jù)后,數(shù)組內(nèi)存地址發(fā)生了變化,內(nèi)存地址從原來的 962 變成了 968,這是因為 CopyOnWriteArrayList 的 add 操作,會生成新的數(shù)組,所以數(shù)組的內(nèi)存地址發(fā)生了變化:

link

迭代繼續(xù)進(jìn)行時,我們發(fā)現(xiàn)迭代器中的地址仍然是迭代之前引用的地址,是 962,而不是新的數(shù)組的內(nèi)存地址:

link

從上面 4 張截圖,我們可以得到迭代過程中,即使 CopyOnWriteArrayList 的結(jié)構(gòu)發(fā)生變動了,也不會拋出 ConcurrentModificationException 異常的原因:CopyOnWriteArrayList 迭代持有的是老數(shù)組的引用,而 CopyOnWriteArrayList 每次的數(shù)據(jù)變動,都會產(chǎn)生新的數(shù)組,對老數(shù)組的值不會產(chǎn)生影響,所以迭代也可以正常進(jìn)行。

6 總結(jié)

當(dāng)我們需要在線程不安全場景下使用 List 時,建議使用 CopyOnWriteArrayList,CopyOnWriteArrayList 通過鎖 + 數(shù)組拷貝 + volatile 之間的相互配合,實(shí)現(xiàn)了 List 的線程安全,我們拋棄 Java 的這種實(shí)現(xiàn),如果讓我們自己實(shí)現(xiàn),你又將如何實(shí)現(xiàn)呢?

到此這篇關(guān)于Java中的CopyOnWriteArrayList深入解讀的文章就介紹到這了,更多相關(guān)CopyOnWriteArrayList深入解讀內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Spring詳細(xì)講解事務(wù)失效的場景

    Spring詳細(xì)講解事務(wù)失效的場景

    實(shí)際項目開發(fā)中,如果涉及到多張表操作時,為了保證業(yè)務(wù)數(shù)據(jù)的一致性,大家一般都會采用事務(wù)機(jī)制,好多小伙伴可能只是簡單了解一下,遇到事務(wù)失效的情況,便會無從下手,下面這篇文章主要給大家介紹了關(guān)于Spring事務(wù)失效場景的相關(guān)資料,需要的朋友可以參考下
    2022-07-07
  • Java如何向主函數(shù)main中傳入?yún)?shù)

    Java如何向主函數(shù)main中傳入?yún)?shù)

    這篇文章主要介紹了Java如何向主函數(shù)main中傳入?yún)?shù),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-02-02
  • java中處理stream.filter()的實(shí)例代碼

    java中處理stream.filter()的實(shí)例代碼

    stream()是Java 8中的一個函數(shù)式接口,用于處理數(shù)據(jù)流,它可以從一個數(shù)據(jù)源,如集合,數(shù)組等生成一個流,這篇文章主要給大家介紹了關(guān)于java中處理stream.filter()的相關(guān)資料,需要的朋友可以參考下
    2024-08-08
  • java中的匿名內(nèi)部類詳細(xì)總結(jié)

    java中的匿名內(nèi)部類詳細(xì)總結(jié)

    以下是對java中的匿名內(nèi)部類進(jìn)行了詳細(xì)的總結(jié)介紹,需要的朋友可以過來參考下,希望對大家有所幫助
    2013-10-10
  • springboot自定義stater啟動流程

    springboot自定義stater啟動流程

    這篇文章主要介紹了springboot自定義stater啟動流程,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下
    2019-12-12
  • Spring Boot中的SpringSecurity基礎(chǔ)教程

    Spring Boot中的SpringSecurity基礎(chǔ)教程

    Spring Security是一個功能強(qiáng)大且高度可定制的身份驗證和訪問控制框架。它實(shí)際上是保護(hù)基于spring的應(yīng)用程序的標(biāo)準(zhǔn)Spring Security是一個框架,側(cè)重于為Java應(yīng)用程序提供身份驗證和授權(quán),這篇文章主要介紹了Spring Boot中的SpringSecurity學(xué)習(xí),需要的朋友可以參考下
    2023-01-01
  • SpringBoot使用Redis進(jìn)行限流功能實(shí)現(xiàn)

    SpringBoot使用Redis進(jìn)行限流功能實(shí)現(xiàn)

    Spring Boot中使用Redis實(shí)現(xiàn)限流功能是一種常見的做法,特別是在高并發(fā)場景下,限流可以有效防止系統(tǒng)過載,保證服務(wù)的穩(wěn)定性,本文就來詳細(xì)的介紹一下SpringBoot使用Redis進(jìn)行限流功能實(shí)現(xiàn),感興趣的可以了解一下
    2025-10-10
  • java通過方法來交換實(shí)參的值實(shí)現(xiàn)方式

    java通過方法來交換實(shí)參的值實(shí)現(xiàn)方式

    文章介紹了Java方法參數(shù)傳遞方式以及如何通過數(shù)組、自定義包裝類或返回結(jié)果來實(shí)現(xiàn)變量交換,同時,討論了內(nèi)部類中static關(guān)鍵字的作用和使用場景
    2025-12-12
  • Jmeter配置代理實(shí)現(xiàn)錄制過程圖解

    Jmeter配置代理實(shí)現(xiàn)錄制過程圖解

    這篇文章主要介紹了Jmeter配置代理實(shí)現(xiàn)錄制過程圖解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-08-08
  • mybatis中的異常BindingException詳解

    mybatis中的異常BindingException詳解

    這篇文章主要介紹了mybatis中的異常BindingException詳解,此異常是mybatis中拋出的,意思是使用的這個方法找到,但是因為mapperScan()已經(jīng)掃描到了Mapper類了,在綁定Mapper.xml時沒有綁定到導(dǎo)致的,需要的朋友可以參考下
    2024-01-01

最新評論

高密市| 兴业县| 太白县| 永兴县| 顺平县| 古交市| 望都县| 乌拉特前旗| 和田市| 莎车县| 紫阳县| 门头沟区| 故城县| 忻州市| 上杭县| 丹阳市| 舒城县| 启东市| 焦作市| 安阳县| 湛江市| 喀喇沁旗| 平顺县| 巴塘县| 田阳县| 界首市| 崇礼县| 红河县| 洛阳市| 松阳县| 德江县| 将乐县| 旬邑县| 渑池县| 沧州市| 保定市| 额敏县| 磴口县| 清涧县| 嘉鱼县| 连平县|