1. 程式人生 > >第三章 CopyOnWriteArrayList原始碼解析

第三章 CopyOnWriteArrayList原始碼解析

注:在看這篇文章之前,如果對ArrayList底層不清楚的話,建議先去看看ArrayList原始碼解析。

1、對於CopyOnWriteArrayList需要掌握以下幾點

  • 建立:CopyOnWriteArrayList()
  • 新增元素:即add(E)方法
  • 獲取單個物件:即get(int)方法
  • 刪除物件:即remove(E)方法
  • 遍歷所有物件:即iterator(),在實際中更常用的是增強型的for迴圈去做遍歷

注:CopyOnWriteArrayList是一個執行緒安全,讀操作時無鎖的ArrayList。

2、建立

public CopyOnWriteArrayList()

使用方法:

List<String> list = new CopyOnWriteArrayList<String>();

相關原始碼:

    private volatile transient Object[] array;//底層資料結構

    /**
     * 獲取array
     */
    final Object[] getArray() {
        return array;
    }

    /**
     * 設定Object[]
     */
    final void setArray(Object[] a) {
        array 
= a; } /** * 建立一個CopyOnWriteArrayList * 注意:建立了一個0個元素的陣列 */ public CopyOnWriteArrayList() { setArray(new Object[0]); }
View Code

注意點:

  • 設定一個容量為0的Object[];ArrayList會創造一個容量為10的Object[]

3、新增元素

public boolean add(E e)

使用方法:

list.add("hello");

原始碼:

    /**
     * 在陣列末尾新增元素
     * 1)獲取鎖
     * 2)上鎖
     * 3)獲取舊陣列及其長度
     * 4)建立新陣列,容量為舊陣列長度+1,將舊陣列拷貝到新陣列
     * 5)將要增加的元素加入到新陣列的末尾,設定全域性array為新陣列
     */
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;//這裡為什麼不直接用this.lock(即類中已經初始化好的鎖)去上鎖
        lock.lock();//上鎖
        try {
            Object[] elements = getArray();//獲取當前的陣列
            int len = elements.length;//獲取當前陣列元素
            /*
             * Arrays.copyOf(elements, len + 1)的大致執行流程:
             * 1)建立新陣列,容量為len+1,
             * 2)將舊陣列elements拷貝到新陣列,
             * 3)返回新陣列
             */
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;//新陣列的末尾元素設成e
            setArray(newElements);//設定全域性array為新陣列
            return true;
        } finally {
            lock.unlock();//解鎖
        }
    }
View Code

注意點:

  • Arrays.copyOf(T[] original, int newLength)該方法在ArrayList中講解過

疑問:

  • 在add(E)方法中,為什麼要重新定義一個ReentrantLock,而不直接使用那個定義的類變數鎖(全域性鎖)
    • 答:事實上,按照他那樣寫,即使是在add、remove、set中存在多個引用,最後也是一個例項this.lock,所以不管你在add、remove、set中怎樣去從新定義一個ReentrantLock,其實add、remove、set中最後使用的都是同一個鎖this.lock,也就是說,同一時刻,add/remove/set只能有一個在執行。這樣講,就是說,下邊這段程式碼完全可以做一個修改。修改前的程式碼:
          public boolean add(E e) {
              final ReentrantLock lock = this.lock;//這裡為什麼不直接用this.lock(即類中已經初始化好的鎖)去上鎖
              lock.lock();//上鎖
      View Code

      修改後的程式碼:

          public boolean add(E e) {
              //final ReentrantLock lock = this.lock;//這裡為什麼不直接用this.lock(即類中已經初始化好的鎖)去上鎖
              this.lock.lock();//上鎖
      View Code
  • 根據以上程式碼可知,每增加一個新元素,都要進行一次陣列的複製消耗,那為什麼每次不將陣列的元素設大(比如說像ArrayList那樣,設定為原來的1.5倍+1),這樣就會大大減少因為陣列元素複製所帶來的消耗?

4、獲取元素

public E get(int index)

使用方法:

list.get(0)

原始碼:

    /**
     * 根據下標獲取元素
     * 1)獲取陣列array
     * 2)根據索引獲取元素
     */
    public E get(int index) {
        return (E) (getArray()[index]);
    }
View Code

注意點:

  • 獲取不需要加鎖

疑問:在《分散式Java應用:基礎與實踐》一書中作者指出:讀操作會發生髒讀,為什麼?

從類屬性部分,我們可以看到array陣列是volatile修飾的,也就是當你對volatile進行寫操作後,會將寫過後的array陣列強制重新整理到主記憶體,在讀操作中,當你讀出陣列(即getArray())時,會強制從主記憶體將array讀到工作記憶體,所以應該不會發生髒讀才對呀!!!

 補:volatile的介紹見《附2 volatile》,連結如下:

5、刪除元素

public boolean remove(Object o)

使用方法:

list.remove("hello")

原始碼:

    /**
     * 刪除list中的第一個o
     * 1)獲取鎖、上鎖
     * 2)獲取舊陣列、舊陣列的長度len
     * 3)如果舊陣列長度為0,返回false
     * 4)如果舊陣列有值,建立新陣列,容量為len-1
     * 5)從0開始遍歷陣列中除了最後一個元素的所有元素
     * 5.1)將舊陣列中將被刪除元素之前的元素複製到新陣列中,
     * 5.2)將舊陣列中將被刪除元素之後的元素複製到新陣列中
     * 5.3)將新陣列賦給全域性array
     * 6)如果是舊陣列的最後一個元素要被刪除,則
     * 6.1)將舊陣列中將被刪除元素之前的元素複製到新陣列中
     * 6.2)將新陣列賦給全域性array
     */
    public boolean remove(Object o) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();//獲取原陣列
            int len = elements.length;//獲取原陣列長度
            if (len != 0) {//如果有資料
                // Copy while searching for element to remove
                // This wins in the normal case of element being present
                int newlen = len - 1;//新陣列長度為原陣列長度-1
                Object[] newElements = new Object[newlen];//建立新陣列

                for (int i = 0; i < newlen; ++i) {//遍歷新陣列(不包含最後一個元素)
                    if (eq(o, elements[i])) {
                        // 將舊陣列中將被刪除元素之後的元素複製到新陣列中
                        for (int k = i + 1; k < len; ++k)
                            newElements[k - 1] = elements[k];
                        setArray(newElements);//將新陣列賦給全域性array
                        return true;
                    } else
                        newElements[i] = elements[i];//將舊陣列中將被刪除元素之前的元素複製到新陣列中
                }

                if (eq(o, elements[newlen])) {//將要刪除的元素時舊陣列中的最後一個元素
                    setArray(newElements);
                    return true;
                }
            }
            return false;
        } finally {
            lock.unlock();
        }
    }
View Code

判斷兩個物件是否相等:

    /**
     * 判斷o1與o2是否相等
     */
    private static boolean eq(Object o1, Object o2) {
        return (o1 == null ? o2 == null : o1.equals(o2));
    }
View Code

注意點:

  • 需要加鎖
  • ArrayList的remove使用了System.arraycopy(這是一個native方法),而這裡沒使用,所以理論上這裡的remove的效能要比ArrayList的remove要低

6、遍歷所有元素

iterator()  hasNext()  next()

使用方法:

講解用的:

        Iterator<String> itr = list.iterator();
        while(itr.hasNext()){
            System.out.println(itr.next());
        }
View Code

實際中使用的:

        for(String str : list){
            System.out.println(str);
        }
View Code

原始碼:

    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
View Code
    private static 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;//0~snapshot.length-1
        }
        
        public E next() {
            if (!hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }
View Code

說明:這一塊兒程式碼非常簡單,看看程式碼註釋就好。

注意:

由於遍歷的只是全域性陣列的一個副本,即使全域性陣列發生了增刪改變化,副本也不會變化,所以不會發生併發異常。但是,可能在遍歷的過程中讀到一些剛剛被刪除的物件。

注意點:

總結:

  • 執行緒安全,讀操作時無鎖的ArrayList
  • 底層資料結構是一個Object[],初始容量為0,之後每增加一個元素,容量+1,陣列複製一遍
  • 增刪改上鎖、讀不上鎖
  • 遍歷過程由於遍歷的只是全域性陣列的一個副本,即使全域性陣列發生了增刪改變化,副本也不會變化,所以不會發生併發異常
  • 讀多寫少髒資料影響不大併發情況下,選擇CopyOnWriteArrayList

疑問:

  • 每增加一個新元素,都要進行一次陣列的複製消耗,那為什麼每次不將陣列的元素設大(比如說像ArrayList那樣,設定為原來的1.5倍+1),這樣就會大大減少因為陣列元素複製所帶來的消耗?
  • get(int)操作會發生髒讀,為什麼?

相關推薦

CopyOnWriteArrayList原始碼解析

注:在看這篇文章之前,如果對ArrayList底層不清楚的話,建議先去看看ArrayList原始碼解析。 1、對於CopyOnWriteArrayList需要掌握以下幾點 建立:CopyOnWriteArrayList() 新增元素:即add(E)方法 獲取單個物件:即get(int)方法

HashMap原始碼解析

5.1、對於HashMap需要掌握以下幾點 Map的建立:HashMap() 往Map中新增鍵值對:即put(Object key, Object value)方法 獲取Map中的單個物件:即get(Object key)方法 刪除Map中的物件:即remove(Object key)方法 判斷

.NET Core實戰專案之CMS 入門篇-原始碼解析配置檔案及依賴注入

作者:依樂祝 原文連結:https://www.cnblogs.com/yilezhu/p/9998021.html 寫在前面 上篇文章我給大家講解了ASP.NET Core的概念及為什麼使用它,接著帶著你一步一步的配置了.NET Core的開發環境並建立了一個ASP.NET Core的mvc專

機器學習實戰——決策樹(原始碼解析)

機器學習實戰中的內容講的都比較清楚,一般都能看懂,這裡就不再講述了,這裡主要是對程式碼進行解析,如果你很熟悉python,這個可以不用看。 #coding=utf-8 ''' Created on 2016年1月5日 @author: ltc ''' from mat

dubbo內核之ioc源碼解析

etx onload list 實現 cto gets ebean cas start dubbo的IOC具體實現在:T injectExtension(T instance)方法中。該方法只在三個地方被使用: 1 createAdaptiveExtension() 2

現代編譯原理——:抽象語法樹以及原始碼

  轉自: http://www.cnblogs.com/BlackWalnut/p/4508093.html   這是flxe的檔案,檔名稱為tiger.l    %{ #include <string.h> #include "util.h" #

QCad原始碼分析

     QCad處理指令碼的基本流程如下: //將指令碼的工廠函式和指令碼型別註冊到系統當中,【jsfactory--js, pytonfactory--python】 RScriptHandlerRegistry::registerScriptHandl

【Android學習】 · 儲存容量的獲取&xml格式文字的建立與解析

相對佈局:結合RelativeLayout九宮格     表格佈局和絕對佈局不常用,瞭解就好   谷歌替代system.out.println()用Log.v(d<i<w<e)(tag,”文字資訊”)   設定

演算法導論課後習題解析

3.1-1 分情況討論 當f(n)≥g(n)f(n)≥g(n)時,max(f(n),g(n))=f(n)max(f(n),g(n))=f(n),存在c1=12,c2=1,n0>0c1=12,c2=1,n0>0使得 0<c1(f(n)+g(n))≤f(n)≤c2(f(n)+g(n

組合語言王爽(版)檢測點答案和解析

檢測點3.1 (1)  在DEBUG中,用 "D 0:0 lf" 檢視記憶體,結果如下:  0000:0000 70 80 F0 30 EF 60 30 E2-00 80 80 12 66 20 22 60  0000:0010 62 26 E6 D6 CC 2E

ORMLite完全解析)官方文件、自定義查詢構造器 Custom Query Builder

          接著上一篇,下面是第三章的翻譯整理,理解錯誤的地方還請批評指正。  第三章、 自定義查詢構造器 3.1  查詢構造器基礎       下面是使用查詢構造器建立自定義查詢語句的基本步驟。首先,以java常量的形式為屬性設定列名,便於使用它 們進行

十一 AtomicInteger原始碼解析

1、原子類 可以實現一些原子操作 基於CAS 下面就以AtomicInteger為例。 2、AtomicInteger 在沒有AtomicInteger之前,對於一個Integer的執行緒安全操作,是需要使用同步鎖來實現的,當然現在也可以通過ReentrantLock來實現,但是最好最方

【龍書答案】解析(未完成)

Exercise 3.3 Problem 3.3.1 Consult the language reference manuals to determine The sets of characters that form the input a

XML解析器,驗證器,轉換器,編輯器等

xml有這麼多的規則,寫出來的xml文字檔案到底符不符合要求呢? 用人工檢驗的方式效率太低,也容易出錯,所以開發出了程式來驗證。 xml驗證器: XML DTD和XML Schema,後者用來替代前者。 如果 XML 文件存在錯誤,那麼程式就不應當繼

讀構建之法 :軟件工程師的成長

知識點 可維護 vid -s 評估 不同 fun 可靠 科研 本章理論和知識點:評價軟件工程師水平的主要方法 軟件工程把相關的技術和過程統一到一個體系中,叫“軟件開發流程”,軟件開發流程的目的是為了提高軟件開發、運營、維護的效率,以及提升用戶滿意度、軟件的可靠性和可維護性。

Js高設筆記

efi alert html 產生 數據 span mil blog com 第三章 數據類型 P25 1, var message; //age變量尚未聲明 alert(message); //"undefined" alert(age); //產生錯誤

總結

tsp 領域 style 成長 集體 lib con 需要 能夠 本章主要的理論和知識點是評價軟件工程師水平的主要方法、技能的反面以及TSP對個人的要求。 首先,不同的數據能夠從不同方面一個展示軟件工程師的技術和能力,例如,通過完成時間平均值的比較,兩位工程師或許能決出完成

軟件工程師的成長

出發 int 開始 體系 會議 tor 可重復 設計 標準 軟件工程包括了開發、運營、維護軟件的過程中的很多技術、做法、習慣和思想。軟件工程把這些相關的技術和過程統一到一個體系中,叫“軟件開發流程”,軟件開發流程的目的是為了提高軟件開發、運營、維護和效率,以及提升用戶滿意度

構建之法讀書心得

如何 讀書心得 初級 知識 技能 任務 項目 標準 技術   在構建之法第三章中,我們主要學習了個人能力的衡量與發展。   初級軟件工程師有以下幾個成長階段:1、積累軟件開發相關的知識,提升技術技能。                    2、積累問題領域的知識和經驗。  

-- DNS

linux一、DNS基礎配置 *)客戶端配置 vim /etc/resolv.conf 編輯dns配置文件 *)服務端配置 yum install bind -y 安裝dns服務 systemctl stop firewalld 關閉防火墻 systemctl tart name