首先我们都知道ArrayList和LinkedList都是线程不安全的集合类,ArrayList底层数据结构为数组,LinkedList底层数据结构为双向链表,这篇文章将根据部分源代码重点分析这两种集合线程不安全的原因。本篇文章基于JDK1.8分析。
1. ArrayList
1.1 数据结构
// 存放元素的数组
transient Object[] elementData;
// Default initial capacity.
private static final int DEFAULT_CAPACITY = 10;
// 记录数组中元素的数量
private int size;
1.2 核心方法
添加元素
public boolean add(E e) {
/**
* 判断是否需要扩容,这里存在线程安全问题。
* 如果AB两个线程进来,A size = 6,a B size = 6,b
* 未在扩容零界点,可能会有null值
* 在扩容临界点,可能会越界
* 在迭代的时候也可能会出现越界异常,即数组的长度 < size
*/
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
// elementData[size] = e;
// size++;
return true;
}
// 确认容量足够
private void ensureCapacityInternal(int minCapacity) {
if (elementData == 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);
}
遍历元素
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
// 1-将 modCount 赋值给 expectedModCount
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
// 若对List进行并发读写,在1步骤中modCount赋值给expectedModCount后又对List进行了修改操作
// ,那么此时 modCount 和 expectedModCount 的值就不相等
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
1.3 线程不安全分析
ArrayList在并发添加元素时可能出现null值或数组越界的情况,在并发读写时可能会抛出ConcurrentModificationException异常。
- 可能出现null值或值被替换
根据添加元素的源码我们知道扩容是这一行代码 ensureCapacityInternal(size + 1);
- 可能出现数组下标越界
根据源码可以知道ArrayList默认容量是10,当线程1和线程2并发add时若size = 9,此时两个线程都没有触发扩容,若线程1在线程2执行赋值操作前先执行size++,那么线程2赋值时会数组越界。
// size = 9,此时线程1和线程2同时add,
public boolean add(E e) {
//step1- 线程1 和 线程2 同时到达,未触发扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//step2- 线程1赋值成功 // step4 size=10此事线程2赋值失败,数组越界,
elementData[size] = e;
//step3 线程1执行size++,此时size = 10
size++;
return true;
}
- 遍历时ConcurrentModificationException异常
根据以上对遍历方法的分析可以知道modCount的值可能会发生变化。
1.4 线程安全的List
对于线程安全的List集合,可以使用Vector或synchronizedList,其中Vector用同步方法实现线程安全,synchronizedList使用同步代码块实现线程安全。两者的效率不高。
也可以使用CopyOnWriteArrayList来实现线程安全,在add时使用可重入锁,每次添加元素时都将copy一个新数组,然后对属性进行赋值(看源码),所以遍历时也是线程安全的,但是遍历时会丢失当前正在添加的元素。因为copy涉及到系统调用, 所以CopyOnWriteArrayList适用于写少读多的场景。
CopyOnWriteArrayList源码部分
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 将添加前的集合长度+1 copy 到一个新的数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 为新数组赋值新值
newElements[len] = e;
// 将新数组设置成当前集合数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
public void forEach(Consumer<? super E> action) {
if (action == null) throw new NullPointerException();
Object[] elements = getArray();
int len = elements.length;
for (int i = 0; i < len; ++i) {
@SuppressWarnings("unchecked") E e = (E) elements[i];
action.accept(e);
}
}
-
LinkedList
2.1 数据结构
双向循环链表。
// 链表长度
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
2.2 核心方法
添加元素
add方法调用的是linkLast方法,所以LinkedList默认是尾插。
void linkLast(E e) {
// 将添加前链表尾节点赋值给l
final Node<E> l = last;
// 每添加一个元素new 一个节点
final Node<E> newNode = new Node<>(l, e, null);
// 重新设置尾节点
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
2.3 线程不安全分析
通过add方法到源码我们知道在并发添加元素时,首先size++是线程不安全的。其次当多个线程同时获取到相同的尾节点的时候,然后多个线程同时在此尾节点后面插入数据的时候会出现数据覆盖的问题。
当线程1 向链表中添加 元素5,线程2向链表中添加元素6的时候,线程1和2同时取得last节点为4,new 新节点时即线程1和线程2的节点的上一个元素都指向4这个节点,最后哪一个线程的值能覆盖另一个线程的值就看谁后执行 l.next = newNode方法了。