第5章 Java并發(fā)包中并發(fā)List源碼剖析

目錄

介紹

JUC包中的并發(fā)List只有CopyOnWriteArrayList弛随。CopyOnWriteArrayList是一個線程安全的ArrayList,使用了寫時復制策略捅伤,對其進行的修改操作都是在底層的一個復制的數組上進行的缸逃。

源碼解析

初始化

CopyOnWriteArrayList內部包含一個array:

/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;

無參構造函數在內部創(chuàng)建了一個大小為0的object數組作為array的初始值

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

下面看有參構造函數:

// 根據傳入數組創(chuàng)建array對象
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

// 根據集合創(chuàng)建array對象
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        elements = c.toArray();
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elements.getClass() != Object[].class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    setArray(elements);
}

關于“c.toArray might (incorrectly) not return Object[] (see 6260652)”的注釋可參考《JDK1.6集合框架bug:c.toArray might (incorrectly) not return Object[] (see 6260652)》

添加元素

CopyOnWriteList中用來添加元素的函數有add(E e)粗恢、add(int index, E element)穴张、addIfAbsent(E e)等,其原理類似缝呕,下面以add(E e)為例進行講解。

public boolean add(E e) {
    // 獲取獨占鎖
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 獲取array
        Object[] elements = getArray();
        // 復制array到新數組斧散,并將新元素添加到新數組
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        // 用新數組代替原來的數組
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

調用add方法的線程會首先獲取獨占鎖供常,保證同時最多有一個線程調用此方法,其他線程會被阻塞直到鎖被釋放鸡捐。

獲取array后將array復制到一個新數組(從代碼可知新數組的長度比原長度大1栈暇,所以CopyOnWriteArrayList時無界list),并把新增的元素添加到新數組箍镜。

獲取指定位置元素

使用E get(int index)獲取下標為index的元素源祈,如果元素不存在則拋出IndexOutOfBoundException異常煎源。

public E get(int index) {
    return get(getArray(), index);
}

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

final Object[] getArray() {
    return array;
}

獲取指定位置的元素需要兩步:首先獲取array,然后通過下標訪問指定位置的元素香缺。整個過程沒有加鎖手销,在多線程下會出現弱一致性問題。

假設某一時刻CopyOnWriteArrayList中有1图张,2锋拖,3中三個元素,如下圖所示:

由于整個過程未加鎖祸轮,可能導致一個線程x在獲取array后兽埃,另一個線程y進行了remove操作,假設要刪除的元素為3适袜。remove操作首先會獲取獨占鎖柄错,然后進行寫時復制操作,也就是復制一份當前array數組苦酱,然后再復制的數組里面刪除線程x通過get方法要訪問的元素3售貌,之后讓array指向復制的數組。而這時線程x仍持有對原來的array的引用躏啰,導致雖然線程y刪除了元素3趁矾,線程x仍能獲得3這個元素,如圖:

修改指定元素

使用E set(int index, E element)方法修改指定元素的值给僵,如果指定位置的元素不存在則拋出IndexOutOfBoundsException異常:

public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        E oldValue = get(elements, index);

        if (oldValue != element) {
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len);
            newElements[index] = element;
            setArray(newElements);
        } else {
            // Not quite a no-op; ensures volatile write semantics
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

首先獲取獨占鎖毫捣,從而阻止其他線程對array數組進行修改,然后獲取當前數組帝际,并調用get方法獲取指定位置的元素蔓同,如果指定位置的元素值與新值不一致就創(chuàng)建新數組并復制元素,然后在新數組上修改指定位置的元素值并設置新數組到array蹲诀。即使指定位置的元素值與新值一樣斑粱,為了保證volatile語義,也需要重新設置array(此處可參看《CopyOnWriteArrayList與java內存模型》)脯爪。

刪除元素

刪除list里的元素则北,可以使用E remove(int index)、boolean remove(Object o)和boolean remove(Object o, Object[] snapshot, int index)等方法痕慢,其原理類似尚揣,下面以remove(int index)為例進行講解。

public E remove(int index) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        if (numMoved == 0)
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            Object[] newElements = new Object[len - 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index,
                                numMoved);
            setArray(newElements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

首先獲取獨占鎖以保證線程安全掖举,然后獲取要被刪除的元素快骗,并把剩余的元素復制到新數組,之后使用新數組替換原來的數組,最后在返回前釋放鎖方篮。

弱一致性的迭代器

弱一致性指返回迭代器后名秀,其他線程對list的改動對迭代器時不可見的。

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

static final class COWIterator<E> implements ListIterator<E> {

    // array的快照
    private final Object[] snapshot;
    // 數組下標
    private int cursor;

    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }

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

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

調用iterator()方法時實際上會返回一個COWIterator對象藕溅,COWIterator對象的snapshot變量保存了當前l(fā)ist的內容匕得。之所以說snapshot是list的快照是因為雖然snapshot獲得了array的引用,但當其他線程修改了list時蜈垮,array會指向新復制出來的數組耗跛,而snapshot仍指向原來array指向的數組,兩者操作不同的數組攒发,這就是弱一致性调塌。

以下為弱一致性的示例:

public class CopyListTest {
    private static volatile CopyOnWriteArrayList<String> arrayList  = new CopyOnWriteArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        arrayList.add("Java");
        arrayList.add("Scala");
        arrayList.add("Groovy");
        arrayList.add("Kotlin");

        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                arrayList.set(0, "hello");
                arrayList.remove(2);
            }
        });

        // 在修改之前獲取迭代器
        Iterator<String> it = arrayList.iterator();

        threadOne.start();

        // 等待子線程執(zhí)行完畢
        threadOne.join();

        // 迭代
        while(it.hasNext()) {
            System.out.println(it.next());
        }

        System.out.println("=========================================");

        // 再次迭代
        it = arrayList.iterator();
        
        // 迭代
        while(it.hasNext()) {
            System.out.println(it.next());
        }
    }
}

輸出如圖:

由上可知,對list的修改對于首次迭代是不可見的惠猿,這即是弱一致性的體現羔砾。

更多

相關筆記:《Java并發(fā)編程之美》閱讀筆記

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市偶妖,隨后出現的幾起案子姜凄,更是在濱河造成了極大的恐慌,老刑警劉巖趾访,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件态秧,死亡現場離奇詭異,居然都是意外死亡扼鞋,警方通過查閱死者的電腦和手機申鱼,發(fā)現死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來云头,“玉大人捐友,你說我怎么就攤上這事±;保” “怎么了匣砖?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長昏滴。 經常有香客問我猴鲫,道長,這世上最難降的妖魔是什么谣殊? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任拂共,我火速辦了婚禮,結果婚禮上蟹倾,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好鲜棠,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布肌厨。 她就那樣靜靜地躺著,像睡著了一般豁陆。 火紅的嫁衣襯著肌膚如雪柑爸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天盒音,我揣著相機與錄音表鳍,去河邊找鬼。 笑死祥诽,一個胖子當著我的面吹牛譬圣,可吹牛的內容都是我干的。 我是一名探鬼主播雄坪,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼厘熟,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了维哈?” 一聲冷哼從身側響起绳姨,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎阔挠,沒想到半個月后飘庄,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡购撼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年跪削,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片份招。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡切揭,死狀恐怖,靈堂內的尸體忽然破棺而出锁摔,到底是詐尸還是另有隱情廓旬,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布谐腰,位于F島的核電站孕豹,受9級特大地震影響,放射性物質發(fā)生泄漏十气。R本人自食惡果不足惜励背,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望砸西。 院中可真熱鬧叶眉,春花似錦址儒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至饱溢,卻和暖如春喧伞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背绩郎。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工潘鲫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人肋杖。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓溉仑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親兽愤。 傳聞我的和親對象是個殘疾皇子彼念,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內容