jdk11源碼--CopyOnWriteArrayList源碼分析

@[toc]

概述

我們都知道CopyOnWriteArrayList是線程安全的列表,其內部是數組結構,並且適用於讀多寫少的應用場景。
當寫比較頻繁時不要使用CopyOnWriteArrayList,應該使用其他的數據結構代替。
接下來就從源碼角度分析一下爲什麼會有以上的特性。

基本屬性

//鎖
final transient Object lock = new Object();

/** 內部真實存放數據的數據結構,它只能通過getArray/setArray方法訪問. */
private transient volatile Object[] array;

其中lock是CopyOnWriteArrayList實現的關鍵。==類中所有的修改操作都會使用這個全局鎖,保證只有一個線程可以對數據進行修改。==
array使用volatile 變量修飾,確保可見性,一個線程修改以後其他線程可以獲取到最新修改後的值。

創建CopyOnWriteArrayList

創建一個空的CopyOnWriteArrayList時,比較簡單,就是初始化一個長度是0的數組。

public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

add(E e)

添加元素的邏輯也是異常的簡單粗暴:

  • 首先獲取鎖
  • 獲取當前數組的長度
  • 現有數組copy到一個新的數組,新數組長度=現有數組長度+1
  • 將新元素添加到新數組最後一個位置
public boolean add(E e) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        es = Arrays.copyOf(es, len + 1);
        es[len] = e;
        setArray(es);
        return true;
    }
}

add(int index, E element)

想數組中指定位置添加元素。
原理同上,也是需要拷貝一份數組,並且長度+1.
唯一的區別是要拷貝兩次,將index在新數組中的位置預留出來存放新的元素。詳見下面代碼註釋

public void add(int index, E element) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException(outOfBounds(index, len));
        Object[] newElements;
        int numMoved = len - index;//計算需要移動幾個元素。這裏指的是index位置後面的元素需要逐個向後移動一位
        if (numMoved == 0)//新元素追加在末尾
            newElements = Arrays.copyOf(es, len + 1);
        else {
            newElements = new Object[len + 1];
            //通過兩次System.arraycopy對數組進行復制,預留出index在新數組中對應的空位。
            System.arraycopy(es, 0, newElements, 0, index);
            System.arraycopy(es, index, newElements, index + 1,
                             numMoved);
        }
        newElements[index] = element;
        setArray(newElements);
    }
}

System.arraycopy

@HotSpotIntrinsicCandidate
public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

參數:

  • Object src : 原數組
  • int srcPos : 從元數據的起始位置開始
  • Object dest : 目標數組
  • int destPos : 目標數組的開始起始位置
  • int length : 要copy的數組的長度

這是一個native方法,並且是有@HotSpotIntrinsicCandidate修飾的。所以該方法不僅是本地方法,而且是虛擬機固有方法。在虛擬機中通過手工編寫彙編或其他優化方法來進行 Java 數組拷貝,這種方式比起直接在 Java 上進行 for 循環或 clone 是更加高效的。數組越大體現地越明顯。

關於@HotSpotIntrinsicCandidate

這個註解是 HotSpot VM 標準的註解,被它標記的方法表明它爲 HotSpot VM 的固有方法, HotSpot VM 會對其做一些增強處理以提高它的執行性能,比如可能手工編寫彙編或手工編寫編譯器中間語言來替換該方法的實現。雖然這裏被聲明爲 native 方法,但是它跟 JDK 中其他的本地方法實現地方不同,固有方法會在 JVM 內部實現,而其他的會在 JDK 庫中實現。在調用方面,由於直接調用 JVM 內部實現,不走常規 JNI lookup,所以也省了開銷。
由於這需要閱讀JVM虛擬機中的源碼,留作後續再深入研究。

get(int index)

get的方法和通常的數組操作一樣。
注意:這裏沒有加鎖,

public E get(int index) {
  return elementAt(getArray(), index);
}
final Object[] getArray() {
    return array;
}
static <E> E elementAt(Object[] a, int index) {
    return (E) a[index];
}

iterator 迭代器

CopyOnWriteArrayList中提供了迭代器的操作。

注意:==這個迭代器實際上是現有array的一個拷貝,所以他不用添加鎖。但是會有髒數據的情況。因爲在迭代期間,其他線程修改了數據以後,這個迭代器中拷貝的數據看不到最新的數據。==

Iterator()方法和listIterator()方法實現是一樣的,只不過返回值的類型不同,在使用時差別不大:

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

public ListIterator<E> listIterator() {
    return new COWIterator<E>(getArray(), 0);
}

//這裏面所有的修改方法均不可用
static final class COWIterator<E> implements ListIterator<E> {
    /** 數組array的快照 */
    private final Object[] snapshot;
    /** 遊標  */
    private int cursor;

    COWIterator(Object[] es, int initialCursor) {
        cursor = initialCursor;
        snapshot = es;//在這裏進行快照賦值
    }

    public boolean hasNext() {
        return cursor < snapshot.length;
    }

    public boolean hasPrevious() {
        return cursor > 0;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }

    @SuppressWarnings("unchecked")
    public E previous() {
        if (! hasPrevious())
            throw new NoSuchElementException();
        return (E) snapshot[--cursor];
    }

    public int nextIndex() {
        return cursor;
    }

    public int previousIndex() {
        return cursor - 1;
    }

    public void remove() {
        throw new UnsupportedOperationException();
    }

    public void set(E e) {
        throw new UnsupportedOperationException();
    }

    public void add(E e) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        final int size = snapshot.length;
        int i = cursor;
        cursor = size;
        for (; i < size; i++)
            action.accept(elementAt(snapshot, i));
    }
}

編寫一個測試類來驗證這個迭代器:

public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList();
        list.add(1);
        list.add(2);
        list.add(3);

        System.out.println(list.toString());

        new Thread(() -> {
            ListIterator<Integer> integerListIterator = list.listIterator();
            Integer next = integerListIterator.next();
            System.out.println(next+"*******");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            next = integerListIterator.next();
            System.out.println(next+"*******");
        }).start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.clear();
            System.out.println(list.toString());
        }).start();
    }

輸出結果:

[1, 2, 3]
1*******
[]
2*******

可見,在其他線程修改了數據以後,迭代器裏的還是老數據。這也就是上面所說的讀到了髒數據。.

官方文檔中有隊這個iterater的註釋:

 * <p>This is ordinarily too costly, but may be <em>more</em> efficient
 * than alternatives when traversal operations vastly outnumber
 * mutations, and is useful when you cannot or don't want to
 * synchronize traversals, yet need to preclude interference among
 * concurrent threads.  The "snapshot" style iterator method uses a
 * reference to the state of the array at the point that the iterator
 * was created. This array never changes during the lifetime of the
 * iterator, so interference is impossible and the iterator is
 * guaranteed not to throw {@code ConcurrentModificationException}.
 * The iterator will not reflect additions, removals, or changes to
 * the list since the iterator was created.  Element-changing
 * operations on iterators themselves ({@code remove}, {@code set}, and
 * {@code add}) are not supported. These methods throw
 * {@code UnsupportedOperationException}.

大體意思也就是說:迭代器創建的時候會生成一個對數組狀態的引用(快照),也就是COWIterator.snapshot。這個快照在迭代器的聲明週期中不會發生變化,不會收到其他線程的干擾。並且不會拋出ConcurrentModificationException異常。迭代器創建後,不支持修改的操作,也不會反映其他線程對CopyOnWriteArrayList數據的任何變更。

CopyOnWriteArrayList在每次更新時都會複製一份,然後修改。所以老的數據還在,除非被JVM垃圾回收掉。這也是爲什麼上面迭代器可以讀取到數據的原因之一。

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