ArrayList源碼學習筆記(2)

上一篇文章ArrayList源碼學習筆記(1)分析了ArrayList的構造函數和擴容過程,並用demo驗證了一下。

現在看到了add方法和remove方法。

add方法有兩個add(E e) 和 add(int index, E element),分別是在尾部添加元素、在指定index添加元素。

remove方法也有兩個remove(int index) 和 remove(Object o),分別是刪除指定索引的元素、刪除指定元素(如果存在)。

在add(int index, E element)和remove方法中,都有System.arraycopy操作,需要把一些元素整體後移或者前移。

 

源碼比較簡單,邏輯很清晰,沒什麼好說的。但是想到面試的時候總是會問線程安全問題,於是研究了一波。

 

ArrayList是線程安全的嗎?

當然不是。

裏邊沒有鎖、臨界區、volatile等各種爲多線程考慮的同步策略。

 

線程不安全的體現是什麼?或者爲什麼線程不安全?

最怕的就是這種問題,因爲你需要詳實的證據,或者確定的理論依據。

首先你需要知道什麼是線程安全,然後才能說一個類是不是線程安全的。

什麼是線程安全?

在網上和書中找了很多說法,都不統一。乾脆不找準確的了,直接綜合各家說法先定下一個概念。

直觀的說,線程安全就是多線程對一個變量、對象、臨界區進行訪問的時候,不會因爲線程調度問題而產生不符合預期的結果。

能達到線程安全的情況是:變量只讀不可修改、有鎖機制保證同時只能有一個線程對變量進行修改。

ArrayList爲什麼不是線程安全的?

有了上面的理論,現在可以說爲啥ArrayList不是線程安全的了。

但是從哪裏說起呢?因爲ArrayList的線程不安全簡直是案例太多了,但我還是找到了核心點來分析。這個核心點是什麼呢?那就是會發生改變的共享變量。因爲我們的視角其實是在多線程中使用ArrayList,那麼ArrayList的對象其實就是一個共享變量,那麼它的所有成員變量也就都是共享變量了。在裏面挑幾個重要的說一下好了。

比較明顯的兩個變量就是size、elementData數組。這倆肯定是頻繁會發生變化的。

下面構造幾個場景觸發多線程不安全。

1、多線程add
多線程add會有兩種線程不安全的表現:(1)對於size的修改衝突,最終size數比預期小(2)有的線程將size增大後,導致另一個線程使用過程中越界。其實這兩個問題都是對size變量的修改導致的。

話不多說,上例子。

import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;

public class ArrayListTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
        ArrayList<Integer> list = new ArrayList<>(0);

        // 控制主線程等待
        CountDownLatch mainThread = new CountDownLatch(10);
        // 多線程add
        for (int m = 0; m < 10; m++) {
            new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    list.add(i);
                }
                mainThread.countDown();
            }).start();
        }

        mainThread.await();

        System.out.println(list.size());
    }
}

一種結果:

Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: 4
	at java.util.ArrayList.add(ArrayList.java:463)
	at ArrayListTest.lambda$main$0(ArrayListTest.java:14)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 3
	at java.util.ArrayList.add(ArrayList.java:463)
	at ArrayListTest.lambda$main$0(ArrayListTest.java:14)
	at java.lang.Thread.run(Thread.java:748)

另一種結果

95491

2、多線程remove

和多線程add道理是一樣的,size的併發修改會引發問題。

import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;

public class ArrayListTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
        ArrayList<Integer> list = new ArrayList<>(0);

        // 控制主線程阻塞等待
        CountDownLatch mainThread = new CountDownLatch(6);
        // 單線程add
        new Thread(() -> {
            for (int i = 0; i < 10000000; i++) {
                list.add(i);
            }
            mainThread.countDown();
        }).start();

        // 多線程remove
        for (int m = 0; m < 5; m++) {
            new Thread(() -> {
                // sleep4秒,使list裏積累一些數據
                try {
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int i = 0; i < 10000000; i++) {
                    if (list.size() > 0) {
                        list.remove(list.size() - 1);
                    }
                }
                mainThread.countDown();
            }).start();
        }

        mainThread.await();

        System.out.println(list.size());
    }
}

結果如下:

Exception in thread "Thread-2" java.lang.IndexOutOfBoundsException: Index: 9989869, Size: 9988711
	at java.util.ArrayList.rangeCheck(ArrayList.java:657)
	at java.util.ArrayList.remove(ArrayList.java:496)
	at ArrayListTest.lambda$main$1(ArrayListTest.java:27)
	at java.lang.Thread.run(Thread.java:748)

3、ArrayList提供的Iterator,不允許在遍歷過程中產生修改。

Iterator的remove方法和netx方法會檢查在執行過程中是否有修改,如果有修改會報錯。

import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.CountDownLatch;

public class ArrayListTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
        ArrayList<Integer> list = new ArrayList<>(0);

        // 使主線程阻塞等待
        CountDownLatch mainThread = new CountDownLatch(2);
        // 單線程add
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                list.add(0, i);
            }
            mainThread.countDown();
        }).start();
    
        // 單線程遍歷remove
        new Thread(() -> {
            // 等add方法執行5秒,數據積累一些
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Iterator iterator = list.iterator();
            while (iterator.hasNext()) {
                iterator.next();
                // 這個sleep位置在這裏,容易觸發remove方法裏對modcount的檢測異常
                // 這個sleep位置在next方法之前,容易觸發next方法裏對modcount的檢測異常
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                iterator.remove();
            }
            mainThread.countDown();
        }).start();

        mainThread.await();

        list.forEach(System.out::println);
    }
}

程序運行結果如下:

Exception in thread "Thread-1" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.remove(ArrayList.java:873)
	at ArrayListTest.lambda$main$1(ArrayListTest.java:53)
	at java.lang.Thread.run(Thread.java:748)

4、分別有單獨的線程進行add和remove

其實,我理解add和remove之間也應該有衝突纔對。因爲add和remove都會有System.arraycopy操作對數據進行移動,這個應該不是原子的操作,在移動過程中應該是會產生衝突。但具體是產生什麼樣的衝突我沒有想清楚,因爲這個方法是native的,沒看到它的源碼,也不知道它是怎麼做的。

設計了下面的代碼,但是沒有發現有什麼錯誤。(專門用了add(0,i)使每次插入都會調用Syatem.arraycopy;使用remove(0)也會使每次刪除都會調用System.arraycopy)

import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;

public class ArrayListTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
        ArrayList<Integer> list = new ArrayList<>(0);

        // 使主線程阻塞等待
        CountDownLatch mainThread = new CountDownLatch(5);
        // 單線程add
        new Thread(() -> {
            for (int i = 0; i < 10000000; i++) {
                list.add(0, i);
            }
            mainThread.countDown();
        }).start();

        // 單線程remove
        new Thread(() -> {
            for (int i = 0; i < 10000000; i++) {
                if (list.size() > 0) {
                    list.remove(0);
                }
            }
            mainThread.countDown();
        }).start();

        mainThread.await();
        System.out.println(list.size());
    }
}

 

 

 

 

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