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