ArrayList 的 add()方法的 操作源码。
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
ArrayList 的不安全主要体现在两个方面。
- 第一种情况:
elementData[size++] = e;
//不是一个原子操作,是分两步执行的。
elementData[size] = e;
size++;
- 单线程执行这段代码完全没问题,可是到多线程环境下可能就有问题了。可能一个线程会覆盖另一个线程的值。
- 可能会发生并发修改异常
- 情况如下:
列表为空 size = 0。
线程 A 执行完 elementData[size] = e;之后挂起。A 把 "a" 放在了下标为 0 的位置。此时 size = 0。
线程 B 执行 elementData[size] = e; 因为此时 size = 0,所以 B 把 "b" 放在了下标为 0 的位置,于是刚好把 A 的数据给覆盖掉了。
线程 B 将 size 的值增加为 1。
线程 A 将 size 的值增加为 2。
这样子,当线程 A 和线程 B 都执行完之后理想情况下应该是 "a" 在下标为 0 的位置,"b" 在标为 1 的位置。而实际情况确是下标为 0 的位置为 "b",下标为 1 的位置啥也没有。
- 第二种情况:
ArrayList 默认数组大小为 10。假设现在已经添加进去 9 个元素了,size = 9。
线程 A 执行完 add 函数中的ensureCapacityInternal(size + 1)挂起了。
线程 B 开始执行,校验数组容量发现不需要扩容。于是把 "b" 放在了下标为 9 的位置,且 size 自增 1。此时 size = 10。
线程 A 接着执行,尝试把 "a" 放在下标为 10 的位置,因为 size = 10。但因为数组还没有扩容,最大的下标才为 9,所以会抛出数组越界异常 ArrayIndexOutOfBoundsException
- 解决Arraylist的方法:
- 用Corrections工具类下的synchronizedList()
List<String> list=Collections.synchronizedList(new ArrayList);
- 使用CopyOnWriteArrayList类(写时复制,读写分离)
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
public boolean add(E e) {
//1、先加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//2、拷贝数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//3、将元素加入到新数组中
newElements[len] = e;
//4、将array引用指向到新数组
setArray(newElements);
return true;
} finally {
//5、解锁
lock.unlock();
}
}
CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的。
这样做是为了避免在多线程并发add的时候,复制出多个副本出来,把数据搞乱了,导致最终的数组数据不是我们期望的。
线程并发的写,则通过锁来控制,如果有线程并发的读,则分几种情况:
1、如果写操作未完成,那么直接读取原数组的数据;
2、如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
3、如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。