多線程場景下如何使用 ArrayList


ArrayList 不是線程安全的,這點很多人都知道,但是線程不安全的原因及表現,怎么在多線程情況下使用ArrayList,可能不是很清楚,這里總結一下。

1. 源碼分析

查看 ArrayList 的 add 操作源碼如下:

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
      // 判斷列表的capacity容量是否足夠,是否需要擴容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 將元素添加進列表的元素數組里面
      elementData[size++] = e;
        return true;
    }

源碼中涉及的幾個元素及方法定義如下:

      /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;
  
/**    * 列表元素集合數組 * 如果新建ArrayList對象時沒有指定大小,那么會將EMPTY_ELEMENTDATA賦值給elementData, * 並在第一次添加元素時,將列表容量設置為DEFAULT_CAPACITY    */ transient Object[] elementData; /**    *列表大小,elementData中存儲的元素個數    */ private int size;  private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); 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的實現主要就是用了一個Object的數組,用來保存所有的元素,以及一個size變量用來保存當前數組中已經添加了多少元素。

執行add方法時,主要分為兩步:

  • 首先判斷elementData數組容量是否滿足需求——》判斷如果將當前的新元素加到列表后面,列表的elementData數組的大小是否滿足,如果size + 1的這個需求長度大於了elementData這個數組的長度,那么就要對這個數組進行擴容; 
  • 之后在elementData對應位置上設置元素的值。

2. 線程不安全的兩種體現

2.1 數組越界異常 ArrayIndexOutOfBoundsException

由於ArrayList添加元素是如上面分兩步進行,可以看出第一個不安全的隱患,在多個線程進行add操作時可能會導致elementData數組越界。

具體邏輯如下:

  1. 列表大小為9,即size=9
  2. 線程A開始進入add方法,這時它獲取到size的值為9,調用ensureCapacityInternal方法進行容量判斷。
  3. 線程B此時也進入add方法,它獲取到size的值也為9,也開始調用ensureCapacityInternal方法。
  4. 線程A發現需求大小為10,而elementData的大小就為10,可以容納。於是它不再擴容,返回。
  5. 線程B也發現需求大小為10,也可以容納,返回。
  6. 線程A開始進行設置值操作, elementData[size++] = e 操作。此時size變為10。
  7. 線程B也開始進行設置值操作,它嘗試設置elementData[10] = e,而elementData沒有進行過擴容,它的下標最大為9。於是此時會報出一個數組越界的異常ArrayIndexOutOfBoundsException.

2.2 元素值覆蓋和為空問題

elementData[size++] = e 設置值的操作同樣會導致線程不安全。從這兒可以看出,這步操作也不是一個原子操作,它由如下兩步操作構成:

elementData[size] = e;
size = size + 1;

在單線程執行這兩條代碼時沒有任何問題,但是當多線程環境下執行時,可能就會發生一個線程的值覆蓋另一個線程添加的值,具體邏輯如下:

  1. 列表大小為0,即size=0
  2. 線程A開始添加一個元素,值為A。此時它執行第一條操作,將A放在了elementData下標為0的位置上。
  3. 接着線程B剛好也要開始添加一個值為B的元素,且走到了第一步操作。此時線程B獲取到size的值依然為0,於是它將B也放在了elementData下標為0的位置上。
  4. 線程A開始將size的值增加為1
  5. 線程B開始將size的值增加為2

這樣線程AB執行完畢后,理想中情況為size為2,elementData下標0的位置為A,下標1的位置為B。而實際情況變成了size為2,elementData下標為0的位置變成了B,下標1的位置上什么都沒有。並且后續除非使用set方法修改此位置的值,否則將一直為null,因為size為2,添加元素時會從下標為2的位置上開始。

3. 代碼示例

如下,通過兩個線程對ArrayList添加元素,復現上面的兩種不安全情況。

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

public class ArrayListSafeTest {

    public static void main(String[] args) throws InterruptedException {

        final List<Integer> list = new ArrayList<Integer>();
        // 線程A將1-1000添加到列表
        new Thread(new Runnable() {

            @Override
            public void run() {
                for (int i = 1; i < 1000; i++) {
                    list.add(i);

                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }

        }).start();
        
        // 線程B將1001-2000添加到列表
        new Thread(new Runnable() {

            @Override
            public void run() {
                for (int i = 1001; i < 2000; i++) {
                    list.add(i);

                    try {
                        Thread.sleep(1); 
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }

        }).start();
        
        Thread.sleep(1000);

        // 打印所有結果
        for (int i = 0; i < list.size(); i++) {
            System.out.println("第" + (i + 1) + "個元素為:" + list.get(i));
        }
    }
}

執行過程中,兩種情況出現如下:

4. ArrayList線程安全處理

4.1 Collections.synchronizedList

最常用的方法是通過 Collections 的 synchronizedList 方法將 ArrayList 轉換成線程安全的容器后再使用。

List<Object> list =Collections.synchronizedList(new ArrayList<Object>);

4.2 為list.add()方法加鎖

synchronized(list.get()) {
list.get().add(model);
}

4.3 CopyOnWriteArrayList

使用線程安全的 CopyOnWriteArrayList 代替線程不安全的 ArrayList。

List<Object> list1 = new CopyOnWriteArrayList<Object>();

4.4 使用ThreadLocal

使用ThreadLocal變量確保線程封閉性(封閉線程往往是比較安全的, 但由於使用ThreadLocal封裝變量,相當於把變量丟進執行線程中去,每new一個新的線程,變量也會new一次,一定程度上會造成性能[內存]損耗,但其執行完畢就銷毀的機制使得ThreadLocal變成比較優化的並發解決方案)。

ThreadLocal<List<Object>> threadList = new ThreadLocal<List<Object>>() {
        @Override
         protected List<Object> initialValue() {
              return new ArrayList<Object>();
         }
 };

 

 

參考:https://blog.csdn.net/u012859681/article/details/78206494

https://www.cnblogs.com/mabaoqing/p/7446938.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM