一、ArrayList
先说结论,ArrayList是线程不安全的。至于为什么需要去了解它的实现原理,来看下它的源码。
首先ArrayList是基于数据实现的,分析它的线程安全问题需要看下add方法
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//真正存储元素的数组
transient Object[] elementData; // non-private to simplify nested class access
//list当前的元素个数
private int size;
//添加元素的方法
public boolean add(E e) {
//这个方法确保elementData数组有足够的容量来存储元素e,也就是如果容量不够会进行扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//把元素e放入数组size下标处并让size自增,
//在多线程环境下++操作不是原子的,所以会产生线程安全问题,所以多个线程同时给size自增时
//最后得到的size是不正确的且结果不确定,那下次执行add时要往size处放元素,放的位置也就是不确定的
//假设3个线程同时放,3次++操作得到的size可能都是1,最后三个线程都往1处放元素,相当于丢了2个元素
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//先判断elementData是不是空,这个判断只有第一次add时才会进入
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// elementData是空需要计算出需要的最小容量,传进来的是size+1,DEFAULT_CAPACITY是10
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 static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
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);
}
//这个扩容的过程也是线程不安全的,假设线程t1计算出newCapacity后线程上下文切换了,其他线程又add了很多
//元素这时容量可能已经变了,t1再恢复执行继续复制数组元素时就会出问题。
}
通过上边对源码的分析可以了解到ArrayList确实是线程不安全的。
二、线程安全的list实现
2.1 Vector
Vector 是一个线程安全的实现,它保证线程安全的方式就是每个方法都是用synchronized修饰的。
2.2 java.util.Collections#synchronizedList(java.util.List)
这个工具方法接收一个list,然后会对这个list进行包装,让每个方法都被synchronized修饰,
所以锁对象是当前List对象,读写不能同时进行。
2.3 CopyOnWriteArrayList
这个list实现内部有一个ReentrantLock,在写数据的时候先加锁,然后会把内部的数组复制一份进行写操作,通过这种方式保障线程安全,具体需要看下源码.
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/** 锁对象 */
final transient ReentrantLock lock = new ReentrantLock();
//存元素的数组
private transient volatile Object[] array;
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//添加元素时先加锁
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//复制旧数组的元素到新数组,容量+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
//把元素e添加到新数组中
newElements[len] = e;
//把新数组赋值给存元素的数组arr
setArray(newElements);
return true;
} finally {
//释放锁
lock.unlock();
}
}
//获取元素的方法不加锁,存的同时可以进行获取,性能高
public E get(int index) {
return get(getArray(), index);
}
}