上一篇文章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());
}
}