ArrayList源碼解析&ConcurrentModificationException

最近在寫一個sdk的時候,沒有考慮到多線程環境下,忽略了對同一個list存在同時進行排序的case,造成線上會出現ConcurrentModificationException的錯誤,因此抽個時間看了一下ArrayList的源碼,總結了幾個比較有意思的地方。

ArrayList實現了幾個接口?

List<E>, RandomAccess, Cloneable, java.io.Serializable 4個接口

我看到RandomAccess比較好奇,打開它裏面是空的,什麼方法都沒有,爲什麼需要一個這樣的接口呢,後來找找資料發現 RandomAccess只是一個標記接口,用於標明實現該接口的List支持快速隨機訪問,主要目的是使排序算法能夠選擇更加 合適的算法,LinkedList就沒有這個接口,因此排序算法在對ArrayList和LinkedList排序時使用的是不同的算法

ArrayList包含了幾個屬性?

private static final long serialVersionUID = 8683452581122892189L; // 這就是序列化的版本號

private static final int DEFAULT_CAPACITY = 10; // 默認的大小

private static final Object[] EMPTY_ELEMENTDATA = {}; // 長度爲0的空數組 

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 默認空數組

transient Object[] elementData; // 我們都知道ArrayList是用數組實現的,這就是本源

private int size; // list的大小

這裏比較有意思的是EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA兩個的空數組,都是空數組,爲什麼要定義2個,這不是多餘嗎?其實不是,EMPTY_ELEMENTDATA可以理解爲你新建ArrayList的時候指定了初始大小就是0,DEFAULTCAPACITY_EMPTY_ELEMENTDATA則是新建的時候沒有指定初始大小,這2個數組會影響動態擴展的邏輯,下面一個點會介紹到。

ArrayList<Integer> t = new ArrayList(0); //EMPTY_ELEMENTDATA
ArrayList<Integer> t = new ArrayList(); //DEFAULTCAPACITY_EMPTY_ELEMENTDATA

ArrayList怎麼動態擴展的?

就如上所說,你可能開始就定義了一個空list, 這個時候你向list中新增一個元素,看看添加元素的源碼:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 擴展數組大小確保足夠大
    elementData[size++] = e; // 放元素
    return true;
}

這個函數看起來很簡單,核心其實是擴展容量的函數ensureCapacityInternal,看看他是怎麼做的

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { 
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
    }

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

這個時候你就能看出DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA的區別了

EMPTY_ELEMENTDATA:list的變化就會是0,1,2,3,4,5,6.....

DEFAULTCAPACITY_EMPTY_ELEMENTDATA: list的變化就是:0,10,11,12....

這個只是決定數組的大小minCapacity,真正擴容的邏輯是在grow方法中

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 每次都擴展1.5倍,就是它
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0) // 當然還有最大的限制
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

ArrayList什麼時候會出現ConcurrentModificationException?

說起ConcurrentModificationException,就不得不說modCount, 它是存在AbstractList中的一個屬性,它記錄了list被修改的次數,因此你會發現添加、刪除、排序等操作的地方都會有modCount++的代碼。

假如你在ArrayList的源碼中搜ConcurrentModificationException,就會發現很多方法開始先用一個變量expectedModCount保留modCount的值,然後做一些操作,最後再對比expectedModCount和modCount的值,如果不一致就會拋ConcurrentModificationException,因此這個邏輯其實在檢查函數的運行的過程中是否有其他的線程修改了list。

順着這個思路,我就寫了2個線程,一個線程在遍歷list, 一個線程在添加元素到list,最後遍歷的線程會發現在它遍歷的過程中有一個線程修改了list, 馬上就拋出ConcurrentModificationException。

public static void main(String[] args) throws InterruptedException {
    ArrayList<Integer> t = new ArrayList();
    List<Thread> group = new ArrayList<>();
    Thread addThread = new Thread(() -> addItem(t));
    addThread.start();
    Thread sortThread = new Thread(() -> Collections.sort(t));
    sortThread.start();
    addThread.join();
    sortThread.join();
    t.sort(null);
}

private static void addItem(List t) {
    for (int i = 0; i < 100000; i++) {
        t.add(i);
    }
}

使用Vector就能避免ConcurrentModificationException?

有人會說ArrayList是線程非安全的,因此纔會有ConcurrentModificationException, Vector是線程安全的,每個方法都是加鎖的,因此就不會有ConcurrentModificationException,是這樣嗎?

不完全是!! 如果你是通過iterator遍歷的話就不一定了

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

private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;
    public boolean hasNext() {
        // Racy but within spec, since modifications are checked
        // within or after synchronization in next/previous
        return cursor != elementCount;
    }
    // 省略後面的代碼
}

可以看到每次遍歷的返回的iterator都是new出來的,每new出來後,expectedModCount是new的那個時刻的modCount,意思是expectedModCount是私有的變量,這個時候另外一個線程隨便一修改,就會報錯了。

如何解決ConcurrentModificationException

ConcurrentModificationException出現的根本原因是2個線程同時改了同一個list,因此如果沒有特殊需求可以使用CopyOnWriteArrayList代替List, 或者直接拷貝一個統一的list:

List<XXX> copyRules = new ArrayList<>(list);

 

歡迎關注我的個人的博客www.zhijianliu.cn, 虛心求教,有錯誤還請指正輕拍,謝謝

版權聲明:本文出自志健的原創文章,未經博主允許不得轉載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章