CopyOnWriteArrayList源碼解析

来源:https://www.cnblogs.com/CodeBear/archive/2019/03/18/10550240.html
-Advertisement-
Play Games

Java併發包提供了很多線程安全的集合,有了他們的存在,使得我們在多線程開發下,可以和單線程一樣去編寫代碼,大大簡化了多線程開發的難度,但是如果不知道其中的原理,可能會引發意想不到的問題,所以知道其中的原理還是很有必要的。 今天我們來看下Java併發包中提供的線程安全的List,即CopyOnWri ...


Java併發包提供了很多線程安全的集合,有了他們的存在,使得我們在多線程開發下,可以和單線程一樣去編寫代碼,大大簡化了多線程開發的難度,但是如果不知道其中的原理,可能會引發意想不到的問題,所以知道其中的原理還是很有必要的。

今天我們來看下Java併發包中提供的線程安全的List,即CopyOnWriteArrayList。

剛接觸CopyOnWriteArrayList的時候,我總感覺這個集合的名稱有點奇怪:在寫的時候複製?後來才知道它就是在寫的時候進行了複製,所以這個命名還是相當嚴謹的。當然,翻譯成 寫時複製 會更好一些。

我們在研究源碼的時候,可以帶著問題去研究,這樣可能效果會更好,把問題一個一個攻破,也更有成就感,所以在這裡,我先拋出幾個問題:

  1. CopyOnWriteArrayList如何保證線程安全性的。
  2. CopyOnWriteArrayList長度有沒有限制。
  3. 為什麼說CopyOnWriteArrayList是一個寫時複製集合。

我們先來看下CopyOnWriteArrayList的UML圖:

主要方法源碼解析

add

我們可以通過add方法添加一個元素

    public boolean add(E e) {
        //1.獲得獨占鎖
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();//2.獲得Object[]
            int len = elements.length;//3.獲得elements的長度
            Object[] newElements = Arrays.copyOf(elements, len + 1);//4.複製到新的數組
            newElements[len] = e;//5.將add的元素添加到新元素
            setArray(newElements);//6.替換之前的數據
            return true;
        } finally {
            lock.unlock();//7.釋放獨占鎖
        }
    }

    final Object[] getArray() {
        return array;
    }

當調用add方法,代碼會跑到(1)去獲得獨占鎖,因為獨占鎖的特性,導致如果有多個線程同時跑到(1),只能有一個線程成功獲得獨占鎖,並且執行下麵的代碼,其餘的線程只能在外面等著,直到獨占鎖被釋放。

線程獲得到獨占鎖後,執行(2),獲得array,並且賦值給elements ,(3)獲得elements的長度,並且賦值給len,(4)複製elements數組,在此基礎上長度+1,賦值給newElements,(5)將我們需要新增的元素添加到newElements,(6)替換之前的數組,最後跑到(7)釋放獨占鎖。

解析源碼後,我們明白了

  1. CopyOnWriteArrayList是如何保證【寫】時線程安全的?因為用了ReentrantLock獨占鎖,保證同時只有一個線程對集合進行修改操作。
  2. 數據是存儲在CopyOnWriteArrayList中的array數組中的。
  3. 在添加元素的時候,並不是直接往array裡面add元素,而是複製出來了一個新的數組,並且複製出來的數組的長度是 【舊數組的長度+1】,再把舊的數組替換成新的數組,這是尤其需要註意的。

get

    public E get(int index) {
        return get(getArray(), index);
    }
    final Object[] getArray() {
        return array;
    }

我們可以通過調用get方法,來獲得指定下標的元素。

首先獲得array,然後獲得指定下標的元素,看起來沒有任何問題,但是其實這是存在問題的。別忘了,我們現在是多線程的開發環境,不然也沒有必要去使用JUC下麵的東西了。

試想這樣的場景,當我們獲得了array後,把array捧在手心裡,如獲珍寶。。。由於整個get方法沒有獨占鎖,所以另外一個線程還可以繼續執行修改的操作,比如執行了remove的操作,remove和add一樣,也會申請獨占鎖,並且複製出新的數組,刪除元素後,替換掉舊的數組。而這一切get方法是不知道的,它不知道array數組已經發生了天翻地覆的變化,它還是傻乎乎的,看著捧在手心裡的array。。。這就是弱一致性

就像微信一樣,雖然對方已經把你給刪了,但是你不知道,你還是每天打開和她的聊天框,準備說些什麼。。。

set

我們可以通過set方法修改指定下標元素的值。

    public E set(int index, E element) {
        //(1)獲得獨占鎖
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();//(2)獲得array
            E oldValue = get(elements, index);//(3)根據下標,獲得舊的元素

            if (oldValue != element) {//(4)如果舊的元素不等於新的元素
                int len = elements.length;//(5)獲得舊數組的長度
                Object[] newElements = Arrays.copyOf(elements, len);//(6)複製出新的數組
                newElements[index] = element;//(7)修改
                setArray(newElements);//(8)替換
            } else {
                //(9)為了保證volatile 語義,即使沒有修改,也要替換成新的數組
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();//(10)釋放獨占鎖
        }
    }

當我們調用set方法後:

  1. 和add方法一樣,先獲取獨占鎖,同樣的,只有一個線程可以獲得獨占鎖,其他線程會被阻塞。
  2. 獲取到獨占鎖的線程獲得array,並且賦值給elements。
  3. 根據下標,獲得舊的元素。
  4. 進行一個對比,檢查舊的元素是否不等於新的元素,如果成立的話,執行5-8,如果不成立的話,執行9。
  5. 獲得舊數組的長度。
  6. 複製出新的數組。
  7. 修改新的數組中指定下標的元素。
  8. 把舊的數組替換掉。
  9. 為了保證volatile語義,即使沒有修改,也要替換成新的數組。
  10. 不管是否執行了修改的操作,都會釋放獨占鎖。

通過源碼解析,我們應該更有體會:

  1. 通過獨占鎖,來保證【寫】的線程安全。
  2. 修改操作,實際上操作的是array的一個副本,最後才把array給替換掉。

remove

我們可以通過remove刪除指定坐標的元素。

    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;
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                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();
        }
    }

可以看到,remove方法和add,set方法是一樣的,第一步還是先獲取獨占鎖,來保證線程安全性,如果要刪除的元素是最後一個,則複製出一個長度為【舊數組的長度-1】的新數組,隨之替換,這樣就巧妙的把最後一個元素給刪除了,如果要刪除的元素不是最後一個,則分兩次複製,隨之替換。

迭代器

在解析源碼前,我們先看下迭代器的基本使用:

public class Main {public static void main(String[] args) {
        CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
        copyOnWriteArrayList.add("Hello");
        copyOnWriteArrayList.add("copyOnWriteArrayList");
        Iterator<String>iterator=copyOnWriteArrayList.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}

運行結果:
image.png

代碼很簡單,這裡就不再解釋了,我們直接來看迭代器的源碼:

    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
        static final class COWIterator<E> implements ListIterator<E> {
    
        private final Object[] snapshot;
     
        private int cursor;

        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }
        
        // 判斷是否還有下一個元素
        public boolean hasNext() {
            return cursor < snapshot.length;
        }
        
        //獲取下個元素
        @SuppressWarnings("unchecked")
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }

當我們調用iterator方法獲取迭代器,內部會調用COWIterator的構造方法,此構造方法有兩個參數,第一個參數就是array數組,第二個參數是下標,就是0。隨後構造方法中會把array數組賦值給snapshot變數。
snapshot是“快照”的意思,如果Java基礎尚可的話,應該知道數組是引用類型,傳遞的是指針,如果有其他地方修改了數組,這裡應該馬上就可以反應出來,那為什麼又會是snapshot這樣的命名呢?沒錯,如果其他線程沒有對CopyOnWriteArrayList進行增刪改的操作,那麼snapshot就是本身的array,但是如果其他線程對CopyOnWriteArrayList進行了增刪改的操作,舊的數組會被新的數組給替換掉,但是snapshot還是原來舊的數組的引用。也就是說 當我們使用迭代器便利CopyOnWriteArrayList的時候,不能保證拿到的數據是最新的,這也是弱一致性問題。

什麼?你不信?那我們通過一個demo來證實下:

  public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
        copyOnWriteArrayList.add("Hello");
        copyOnWriteArrayList.add("CopyOnWriteArrayList");
        copyOnWriteArrayList.add("2019");
        copyOnWriteArrayList.add("good good study");
        copyOnWriteArrayList.add("day day up");
        new Thread(()->{
            copyOnWriteArrayList.remove(1);
            copyOnWriteArrayList.remove(3);
        }).start();
        TimeUnit.SECONDS.sleep(3);
        Iterator<String> iterator = copyOnWriteArrayList.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

運行結果:
image.png
這沒問題把,我們先是往list裡面add了點數據,然後開一個線程,線上程裡面刪除一些元素,睡3秒是為了保證線程運行完畢。然後獲取迭代器,遍歷元素,發現被remove的元素沒有被列印出來。

然後我們換一種寫法:

   public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
        copyOnWriteArrayList.add("Hello");
        copyOnWriteArrayList.add("CopyOnWriteArrayList");
        copyOnWriteArrayList.add("2019");
        copyOnWriteArrayList.add("good good study");
        copyOnWriteArrayList.add("day day up");
        Iterator<String> iterator = copyOnWriteArrayList.iterator();
        new Thread(()->{
            copyOnWriteArrayList.remove(1);
            copyOnWriteArrayList.remove(3);
        }).start();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

這次我們改變了代碼的順序,先是獲取迭代器,然後是執行刪除線程的操作,最後遍歷迭代器。
運行結果:
image.png
可以看到被刪除的元素,還是列印出來了。

如果我們沒有分析源碼,不知道其中的原理,不知道弱一致性,當在多線程中用到CopyOnWriteArrayList的時候,可能會痛不欲生,想砸電腦,不知道為什麼獲取的數據有時候就不是正確的數據,而有時候又是。所以探究原理,還是挺有必要的,不管是通過源碼分析,還是通過看博客,甚至是直接看JDK中的註釋,都是可以的。

在Java併發包提供的集合中,CopyOnWriteArrayList應該是最簡單的一個,希望通過源碼分析,讓大家有一個信心,原來JDK源碼也是可以讀懂的。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 原型鏈 原型鏈 引入從Object和Function開始 Object和Function都作為JS的自帶函數,Object繼承自己,Funtion繼承自己,Object和Function互相是繼承對方,也就是說Object和Function都既是函數也是對象。 12 console.log(Func ...
  • 數組 數組 1.數組:數組是一組數據(數據類型不限,任意)的有序集合 >我們寫代碼,一般一個數組只放一種數據類型的數據 2.我們寫代碼,一般一個數組只放一種類型的數據 3.註意: 大多數的語言裡面數組的存儲是連續的,但是js的數組特點決定了js的數組不一定是連續的。 數組的特點 1.作用:將許多零散 ...
  • 問:JavaScript 如何查找對象中某個 value 並返迴路徑上所有的 key? 有例如上面這樣一個對象,要求封裝一個函數,傳入對象和某個 value,返回該 value 路徑上的 key。比如:searchKeys(obj, "str3"),得到 "key3, key2"。—— 來源於 @z ...
  • 一、Angular2框架的開發語言 Angular2是谷歌開發的一套前端框架,Angular2就是用Typescript語言的寫的。因此,Typescript語言幫你更好的學習angular2框架。 二、支持ES6 Typescript支持ES6規範的語言,ES6規範指出未來客戶端腳本語言的發展方向 ...
  • 這兩天剛把適配器模式與外觀模式學習了一遍,記錄一下自己在學習中的思考。 適配器設計模式與外觀設計模式所涉及到的一個設計原則: 最少知識原則:不要讓太多的類耦合在一起,以免當修改了某一部分後,會影響到其他部分。 對於任何對象而言,在該對象的方法內,其中最少所指的範圍: 1. 該對象本身; 2.被當作方 ...
  • 題意 "題目鏈接" Sol yy出了一個暴躁線段樹的做法。 因為題目保證了 $a_i + k_i define Pair pair define MP(x, y) make_pair(x, y) define fi first define se second define int long lon ...
  • 先給出十轉二的除法 2 60 30 0 15 0 7 1 3 1 1 1 0 1 60轉二 111100 再介紹位運算符 a=60 b=13 A = 0011 1100 B = 0000 1101 A&b = 0000 1100A | B = 0011 1101A ^ B = 0011 0001~A ...
  • 前言 開心一刻 周末,帶著老婆兒子一起逛公園。兒子一個人跑在前面,吧唧一下不小心摔了一跤,腦袋瓜子摔了個包,稀里嘩啦的哭道:“爸爸,我會不會摔成傻子!” 我指了指我頭上的傷痕安慰道:“不會的,你看,這是爸爸小時候摔的。” 話還沒有說話,小家伙哭的更厲害了:“那就是說我長大後就會和你一樣傻了,我不要, ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...