1. 程式人生 > >併發容器-CopyOnWriteArrayList

併發容器-CopyOnWriteArrayList

本文作者:王一飛,叩丁狼高階講師。原創文章,轉載請註明出處。  

接上篇講的併發中的list, 今天來聊聊CopyOnWriteArrayList。

概念

CopyOnWriteArrayList 類是JDK1.5引入的處理併發操作的容器類,他是Arraylist類的一種執行緒安全的變種,在併發環境下, 保證集合的讀與寫安全。

CopyOnWriteArrayList類是執行緒安全的容器,但它的安全是有一定的限制的。他的執行緒安全操作針對是併發環境下執行緒讀與寫安全。簡單的講, 它只能保證, 一邊執行緒主讀(遍歷/獲取), 一邊執行緒主寫(新增/刪除/修改)操作上的安全。如果一邊執行緒讀寫複合操作,另一邊執行緒也讀寫複合操作,那它也無能為力啦。

案例1:CopyOnWriteArrayList 無法保證非讀寫模式執行緒安全

比如: 上篇測試例子

public class App {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        list.add("1");
        list.add("2");
        new Thread(new Runnable() {
            public void run() {
                //集合大小
                int len = list.size();
                try {
                    //睡5s
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //刪除最後一個
                list.remove(len-1);
            }
        }, "t1").start();

        new Thread(new Runnable() {
            public void run() {
                //清空集合
                list.clear();
            }
        }, "t2").start();
    }
}

將ArrayList換成CopyOnWriteArrayList, 依然存在陣列越界問題, 原因就是上面說的CopyOnWriteArrayList僅保證一執行緒主讀, 一執行緒主寫, , 例子中t1執行緒複合操作,基友讀又有寫難於保證操作安全.

案例2:ConcurrentModificationException 異常解析

再看下面例子: 一個執行緒forEach遍歷list集合, 另一執行緒刪除list集合元素

public class App {
    public static void main(String[] args) {
        //CopyOnWriteArrayList
        final Vector<String> list = new Vector<>();
        //final ArrayList<String> list = new ArrayList<>();
        for(int i =0;i < 1000; i++){
            list.add(i+"");
        }
        new Thread(new Runnable() {
            public void run() {
                for(String s : list){
                    System.out.println(s);
                }
            }
        }, "t1").start();

        new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < list.size(); i++)
                {
                    list.remove(i);
                }
            }
        }, "t2").start();
    }
}

使用普通的ArrayList 或者使用單操作執行緒安全的Vector, 多次執行,有很大概率丟擲一個非常經典的執行緒異常:

Exception in thread "t1" java.util.ConcurrentModificationException
    at java.util.Vector$Itr.checkForComodification(Vector.java:1184)
    at java.util.Vector$Itr.next(Vector.java:1137)
    at cn.wolfcode.ch14.App$1.run(App.java:19)
    at java.lang.Thread.run(Thread.java:745)

分析:
ConcurrentModificationException 異常是Collection 集合定義一個用於限制多執行緒環境下集合併發修改的異常.比如: 某個執行緒在 Collection 上進行迭代時,另一個執行緒試圖修改該 Collection。在這些情況下,第一執行緒迭代結果就不確定了。所以當檢測到這種行為,迭代器選擇丟擲此異常。
來看下原始碼, 它是怎麼折騰: 以ArrayList為例
ArrayList:

 public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
        //關鍵屬性:繼承自父類AbstractList
        protected transient int modCount = 0;
}

modCount 屬性繼承自AbstractList, 用於記錄的是集合被修改(任意能導致集合結構發生變化的操作)次數, 每次修改+1.

 public boolean add(E e) {
        ensureCapacityInternal(size + 1); 
        elementData[size++] = e;
        return true;
    }
 private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

list集合的forEach迭代, 底層使用迭代器模式實現的, 在Arraylist內部維護了一個迭代器類

private class Itr implements Iterator<E> {
        int cursor;       //迭代器遊標, 指向下一個元素索引
        int lastRet = -1; // 最後一個元素索引
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            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];
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
}

在list呼叫forEach操作時回撥用iterator 方法獲取迭代器物件,

public Iterator<E> iterator() {
    return new Itr();
 }

此時回將list持有的modCount賦值給itr類中的expectedModCount 屬性,當使用迭代器迭代list元素時, iter.next( ) 方法會執行, 執行前呼叫checkForComodification 方法, 檢查modCount 是否等於expectedModCount

      final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

如果A執行緒在執行iter.next() 方法前, B執行緒對list集合做修改動作, 此時的modCount 就無法保證等於 expectedModCount , 故丟擲ConcurrentModificationException異常.

回到案例, 執行緒t1做迭代操作, 執行緒t2做刪除操作, 執行緒t2會對modCount進行修改, t1迭代器初始化的expectedModCount 屬性, 肯定無法等於modCount, 所以丟擲ConcurrentModificationException異常.

CopyOnWriteArrayList使用

案例中, list集合使用丟擲ConcurrentModificationException阻止存在併發安全隱患的迭代. 那如果, 存在這麼一場景, 執行緒t1只關心當前時刻持有的list集合的資料迭代. 不關心list的資料是否被修改呢?此時,可以使用CopyOnWriteArrayList作為操作的集合

public class App {

    public static void main(String[] args) {
        //final Vector<String> list = new Vector<>();
        final CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        for(int i =0;i < 1000; i++){
            list.add(i+"");
        }



        new Thread(new Runnable() {
            public void run() {
                for(String s : list){
                    System.out.println(s);
                }
            }
        }, "t1").start();
        new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < list.size(); i++)
                {
                    list.remove(i);
                }
            }
        }, "t2").start();
    }
}

上述案例如果使用CopyOnWriteArrayList作為迭代集合, 執行緒t1可以很愉快輸出0~1000的資料, 它是怎麼做到? 這得從CopyOnWriteArrayList原理說起.

CopyOnWriteArrayList類的設計理念,是基於一種叫Copy-On-Write(簡稱COW)設計思路,通俗的說就是讀與寫分離。具體可以看下圖:
初始
最初CopyOnWriteArrayList使用array資料存放list集合資料
迭代遍歷
當使用迭代器對CopyOnWriteArrayList進行遍歷, 迭代器會給array弄一個快照,snapshot指向與array陣列資料.具體程式碼:CopyOnWriteArrayList 原始碼

    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
    static final class COWIterator<E> implements ListIterator<E> {
        /** Snapshot of the array */
        private final Object[] snapshot;
        private int cursor;
        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }
}

此時, 如果有其它執行緒對相同的list做寫操作, 比如add元素
執行緒寫操作

CopyOnWriteArrayList 將array複製一份賦值個newElments,然後在新的陣列中做寫操作, 操作結束後, 讓array重新指向新陣列.

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
    final Object[] getArray() {
        return array;
    }
    final void setArray(Object[] a) {
        array = a;
    }

注意觀察, 在寫操作時, 使用lock.lock();進行加鎖, 保證寫安全.

回到上面案例, 執行緒t1在迭代時一直使用的最初的array快照snapshot, 執行緒t2的寫操作修改的都是新的array對snapshot沒有任何影響, 所以可以從0列印到999

存在問題

最後說下CopyOnWriteArrayList 的侷限性:
1>每次寫操作都需要複製, 記憶體佔用比重高, 不適宜操作大量資料
2>無法保證資料實時一致, 只能保證最終資料一致.

所以CopyOnWriteArrayList 在讀遠大於寫的場合使用才有一定意義.

到這本篇結束.