併發容器:CopyOnWriteArrayList

0.ConpyOnWriteArrayList的使用場景

在多線程中,若是A線程在讀取一個List時,B線程在向裏面add數據,會拋出異常:java.util.ConcurrentModificationExceptionjava

public class ErrorTest {

    public static void main(String[] args) throws InterruptedException {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

        final ArrayList<Integer> list_thrd = new ArrayList<>(list);
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int count = 0;
                while (true) {
                    list_thrd.add(count++);
                }
            }
        });

        thread.setDaemon(true);
        thread.start();

        Thread.sleep(3);
        for (Integer integer : list_thrd) {
            System.out.println(integer);
        }
    }

}

運行這段代碼,會拋出java.util.ConcurrentModificationException,即主線程在遍歷list元素的時候,子線程在向裏面添加元素。在這種狀況下,咱們可使用CopyOnWriteArrayList進行操做。多線程

1.初步瞭解CopyOnWriteArrayList

CopyOnWrite,字面意思就是寫時複製。通俗的說,就是咱們向容器中添加數據的時候,並非向容器自己添加數據。而是,先copy一個副本,向裏面添加記錄,而後再將copy賦值給原對象。咱們來看下面的代碼併發

public class CopyOnWriteListTest {

    public static void main(String[] args) throws InterruptedException {
        List<Integer> temp = Arrays.asList(1, 2, 3, 4, 5);

        final CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(temp);

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int count = 0;
                while (true) {
                    list.add(count++);
                }
            }
        });

        thread.setDaemon(true);
        thread.start();

        Thread.sleep(3);
        for (Integer integer : list) {
            System.out.println(list.hashCode());
            System.out.println(integer);
        }
    }

}

咱們在遍歷CopyOnWriteArrayList集合的時候,同時打印出hashCode,能夠看到輸出結果:ide

-1813306013
1
1381289098
2
1109838168
3
-1094901371
4
-1008565321
5
-1813169375
0
-1540165649
1
-1441641759
2
1807730922
3
-870434537
4
-1185576945
5

這說明CopyOnWriteArrayList的add方法確實是建立了新的list對象,同時也不會拋出異常。this

2.CopyOnWriteArrayList的實現原理

2.1 add方法

/** The lock protecting all mutators */
transient final ReentrantLock lock = new ReentrantLock();

/** The array, accessed only via getArray/setArray. */
private volatile transient Object[] 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();
        }
    }
  • 添加方法使用了重入鎖來保證多線程添加的時候,不會生成多個list的副本在內存中;
  • array對象使用了volatile修飾,保證了多線程環境下的可見性
  • 最後使用setArray來給array對象賦值

2.2 get方法

private E get(Object[] a, int index) {
        return (E) a[index];
    }

get方法沒有使用鎖,因此讀取的仍是舊的數據,由於寫入的時候並無鎖住舊的array對象spa

3.CopyOnWriteArrayList的使用場景 

從剛剛的例子中咱們能夠看出,CopyOnWriteArrayList適合讀多寫少的場景。例如黑白名單等,將數據初始化到List當中,當有較少寫操做的時候,能夠保證多併發狀況下,List的最終一致性。可是,它也是有缺點的:當執行add操做的時候,因爲要copy出一個對象,這時會致使內存佔用加大。可能會形成頻繁的Young GC和Full GC。線程

相關文章
相關標籤/搜索