JUC之集合中的執行緒安全問題

xbhog發表於2022-01-02

集合執行緒安全問題

JDK Version:9

首先說下集合執行緒安全是什麼:當多個執行緒對同一個集合進行新增和查詢的時候,出現異常錯誤。

復現例子:

package com.JUC;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class ListSecutity04 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,8));

                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

效果圖:

image-20211231201048683

可以看到報ConcurrentModificationException併發修改異常;出現該錯誤的問題是,在ArrayList中的add方法沒有加鎖。

檢視其原始碼:

public boolean add(E e) {
    modCount++;
    add(e, elementData, size);
    return true;
}
//----------------------------------------
public void add(int index, E element) {
    rangeCheckForAdd(index);
    modCount++;
    final int s;
    Object[] elementData;
    if ((s = size) == (elementData = this.elementData).length)
        elementData = grow();
    System.arraycopy(elementData, index,
                     elementData, index + 1,
                     s - index);
    elementData[index] = element;
    size = s + 1;
}
//----------------------------------------
private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)
        elementData = grow();
    elementData[s] = e;
    size = s + 1;
}

解決方式-:Vector和Conllections

見名知意,標題的兩種方法都是比較古老的方法,使用Vector是因為其add方法中加的sychronized關鍵字修飾的

public synchronized boolean add(E e) {
    modCount++;
    add(e, elementData, elementCount);
    return true;
}

另一種方法是在Collection中的工具類synchronizedCollection(Collection<T> c)返回指定 collection 支援的同步(執行緒安全的)collection。

反覆執行測試,程式碼通過:

package com.JUC;

import java.util.*;

public class ListSecutity04 {
    public static void main(String[] args) {

//        List<String> list = new ArrayList<>();
//        List<String> list = new Vector<>();
        List<String> list = Collections.synchronizedList(new ArrayList());
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,8));

                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

雖然但是,我們選擇CopyOnWriteArrayList;

解決方式二:CopyOnWriteArrayList

寫時複製技術:

List<String> list = new CopyOnWriteArrayList<>();

其思想:

在多執行緒的情況下,當對集合進行寫的操作的時候,系統先將原來的內容(A)複製一份為(B),原來的內容(A)可以進行併發讀,複製後的內容(B)寫入新的內容,當內容寫入完成後,A與B實現覆蓋或者說合並。

其中陣列的定義我們使用的是volatile定義的,這樣,每個執行緒可以實時的觀察到陣列的變化。

private transient volatile Object[] array;
final void setArray(Object[] a) {
    array = a;
}

add()方法原始碼:對lock物件加了鎖

final transient Object lock = new Object();
public boolean add(E e) {
    synchronized (lock) {
        Object[] elements = getArray();  //獲取原內容
        int len = elements.length;  
        Object[] newElements = Arrays.copyOf(elements, len + 1); //陣列複製
        newElements[len] = e;  //新增新的內容
        setArray(newElements);  //覆蓋原陣列
        return true;
    }
}

CopyOnWriteArrayList最大特點,讀寫分離,最終一致。比synchronized悲觀鎖效能較好。缺點就是,複製需要佔用記憶體,可能出現OOM的情況。

與之類似執行緒不安全的集合有:HashMap,HashSet,其解決方法類似,在JUC中都有對應,

image-20220101202732087

我們也可以進行程式碼的編寫,並進入原始碼簡單分析一下:

HashSet--CopyOnWriteArraySet

首先檢視HashSet中的add方法的原始碼:(執行緒不安全)

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}
//---------map中
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

從中可以看到,set集合底層使用的是map集合中的put方法,進入其方法中檢視,是沒有同步相關的程式碼修飾的。

也可以看出來Set集合是無序且不重複的,set中傳入的E最後被傳入到map中作為key。

然後檢視下CopyOnWriteArraySet中add的原始碼:(執行緒安全)

public boolean add(E e) {
    return al.addIfAbsent(e);  //CopyOnWriteArrayList<E> al = new CopyOnWriteArrayList<E>()
}
//-------------addIfAbsent---------------
public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
    return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
    addIfAbsent(e, snapshot);
}
//---------------------addIfAbsent------------------------
private boolean addIfAbsent(E e, Object[] snapshot) {
    synchronized (lock) {
        Object[] current = getArray();
        int len = current.length;
        if (snapshot != current) {
            // Optimize for lost race to another addXXX operation
            int common = Math.min(snapshot.length, len);
            for (int i = 0; i < common; i++)
                if (current[i] != snapshot[i]
                    && Objects.equals(e, current[i]))
                    return false;
            if (indexOf(e, current, common, len) >= 0)
                return false;
        }
        Object[] newElements = Arrays.copyOf(current, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    }

從原始碼可以看到,set使用的是CopyOnWriteArrayList,最終新增的資料在addIfAbsent方法中進行了同步。

HashMap--ConcurrentHashMap

HashMap解決的方式就是ConcurrentHashMap,其原始碼中採用的也是synchronized修飾的,具體的新增的流程,程式碼沒讀懂,暫且不表。
歡迎朋友分享下該部分的優質部落格。

相關文章