我们知道ArrayList是线程不安全的,与之对应的线程安全Vector,为何?看源码
ArrayList:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
//ensureCapacityInternal()这个方法的作用就是判断如果将当前的新元素加到列表后面,列表的elementData数组的大小是否满足,如果size + 1的这个需求长度大于了elementData这个数组的长度,那么就要对这个数组进行扩容
elementData[size++] = e;
return true;
}
Vector:
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
一目了然:Vector的add方法加了synchronized ,而ArrayList没有,所以ArrayList线程不安全,但是,由于Vector加了synchronized ,变成了串行,所以效率低。
不安全详细解释:
1.假设size=9
2.线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。
3.线程B此时也进入add方法,它获取到size的值也为9,也开始调用ensureCapacityInternal方法。
4.线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回。
5.线程B也发现需求大小为10,也可以容纳,返回。
6.线程A开始进行设置值操作, elementData[size++] = e 操作。此时size变为10。
7.线程B也开始进行设置值操作,它尝试设置elementData[10] = e,
而elementData没有进行过扩容,它的下标最大为9。
于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException.
另外第二步 elementData[size++] = e 设置值的操作同样会导致线程不安全。因为他不是一个原子操作,它由如下两步操作构成:
1.elementData[size] = e;
2.size = size + 1;
在单线程执行这两条代码时没有任何问题,但是当多线程环境下执行时,可能就会发生一个线程的值覆盖另一个线程添加的值,具体逻辑如下:
1.列表大小为0,即size=0
2.线程A开始添加一个元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。
3.接着线程B刚好也要开始添加一个值为B的元素,且走到了第一步操作。此时线程B获取到size的值依然为0,于是它将B也放在了elementData下标为0的位置上。
4.线程A开始将size的值增加为1
5.线程B开始将size的值增加为2
这样线程AB执行完毕后,理想中情况为size为2,elementData下标0的位置为A,下标1的位置为B。而实际情况变成了size为2,elementData下标为0的位置变成了B,下标1的位置上什么都没有。并且后续除非使用set方法修改此位置的值,否则将一直为null,因为size为2,添加元素时会从下标为2的位置上开始。
解决方案:
1.使用 vector代替ArrayList(不建议)
2.使用Collections提供的方法synchronizedList来保证list是同步线程安全(也不建议)
List<String> list =
Collections.synchronizedList(new ArrayList<>());
此图也说明:Set、Map、List类也是线程不安全
3.使用基于写时复制的CopyOnWriteArrayList
拓展:
List->CopyOnWriteArrayList
Set->CopyOnWriteArraySet
Map->concurrentHashmap 注意:synchronizedMap是表锁,效率低,concurrentHashmap,行锁(只锁写入模块),效率高
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
//写时需要加锁
final transient ReentrantLock lock = new ReentrantLock();
//在修改之后需要保证其他读线程能立刻读到新数据
private transient volatile Object[] array;
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
//增加元素时需要加锁
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1); //复制出一份新的数组,长度加一
newElements[len] = e; //把新元素加在末尾
setArray(newElements); //引用改为新建的副本数组
return true;
} finally {
lock.unlock();
}
}
//获取数组中的元素,一律从旧的数组中读
public E get(int index) {
return get(getArray(), index);
}
}
- 原理:
初始化的时候只有一个容器,很常一段时间,这个容器数据、数量等没有发生变化的时候,大家(多个线程),都是读取(假设这段时间里只发生读取的操作)同一个容器中的数据,所以这样大家读到的数据都是唯一、一致、安全的,但是后来有人往里面增加了一个数据,这个时候CopyOnWriteArrayList 底层实现添加的原理是先copy出一个容器(可以简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了之前那个旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。