arraylist下标从几开始_程序员:Java集合类ArrayList线程不安全验证和解决

概述

对于ArrayList,相信大家也都经常使用。

还记得我以前面试的时候,经常有面试观问集合的知识点,其中一个被经常差距到的就是:ArrayList是否是线程安全的?

答案是三个字:不安全。

那它线程不安全的具体体现又是怎样的呢?我们一起来看一下源码。

源码分析

首先看看这个类所拥有的部分属性字段:

public class ArrayList extends AbstractList

implements List, RandomAccess, Cloneable, java.io.Serializable

{

/**

* 列表元素集合数组

* 如果新建ArrayList对象时没有指定大小,那么会将EMPTY_ELEMENTDATA赋值给elementData,

* 并在第一次添加元素时,将列表容量设置为DEFAULT_CAPACITY

*/

transient Object[] elementData;

/**

* 列表大小,elementData中存储的元素个数

*/

private int size;

}

所以通过这两个字段我们可以看出,ArrayList的实现主要就是用了一个Object的数组,用来保存所有的元素,以及一个size变量用来保存当前数组中已经添加了多少元素。

接着我们看下最重要的add操作时的源代码:

public boolean add(E e) {

/**

* 添加一个元素时,做了如下两步操作

* 1.判断列表的capacity容量是否足够,是否需要扩容

* 2.真正将元素放在列表的元素数组里面

*/

ensureCapacityInternal(size + 1); // Increments modCount!!

elementData[size++] = e;

return true;

}

ensureCapacityInternal()这个方法的详细代码我们可以暂时不看,它的作用就是判断如果将当前的新元素加到列表后面,列表的elementData数组的大小是否满足,如果size + 1的这个需求长度大于了elementData这个数组的长度,那么就要对这个数组进行扩容。

由此看到add元素时,实际做了两个大的步骤:

判断elementData数组容量是否满足需求

在elementData对应位置上设置值

这样也就出现了第一个导致线程不安全的隐患,在多个线程进行add操作时可能会导致elementData数组越界。具体逻辑如下:

列表大小为9,即size=9

线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。

线程B此时也进入add方法,它获取到size的值也为9,也开始调用ensureCapacityInternal方法。

线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回。

线程B也发现需求大小为10,也可以容纳,返回。

线程A开始进行设置值操作, elementData[size++] = e 操作。此时size变为10。

线程B也开始进行设置值操作,它尝试设置elementData[10] = e,而elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException.

另外第二步 elementData[size++] = e 设置值的操作同样会导致线程不安全。从这儿可以看出,这步操作也不是一个原子操作,它由如下两步操作构成:

elementData[size] = e;

size = size + 1;

在单线程执行这两条代码时没有任何问题,但是当多线程环境下执行时,可能就会发生一个线程的值覆盖另一个线程添加的值,具体逻辑如下:

列表大小为0,即size=0

线程A开始添加一个元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。

接着线程B刚好也要开始添加一个值为B的元素,且走到了第一步操作。此时线程B获取到size的值依然为0,于是它将B也放在了elementData下标为0的位置上。

线程A开始将size的值增加为1

线程B开始将size的值增加为2

这样线程AB执行完毕后,理想中情况为size为2,elementData下标0的位置为A,下标1的位置为B。而实际情况变成了size为2,elementData下标为0的位置变成了B,下标1的位置上什么都没有。并且后续除非使用set方法修改此位置的值,否则将一直为null,因为size为2,添加元素时会从下标为2的位置上开始。

ArrayList 线程不安全验证

验证Demo:

/**

* 集合类ArrayList线程不安全验证

*

* @author wangjie

* @version V1.0

* @date 2019/12/17

*/

public class ContainerNotSafe {

public static void main(String[] args) {

List list = new ArrayList<>();

for(int i = 0; i < 30 ; i++){

new Thread(() ->{

list.add(UUID.randomUUID().toString().substring(0,8));

System.out.println(list);

},i+"").start();

}

}

}

运行结果:

579fcc0d3d2868899c1ff633d567facd.png

java.util.ConcurrentModificationException

这个异常各位可是眼熟?

并发修改异常,以下是JDK1.8 文档截图

50c2e873065b4333682baf0b43080466.png
  • 对象被不同的线程同时修改
  • 如果单个线程发出违反对象合同的方法调用序列,则该对象可能会抛出此异常。 例如,如果线程在使用故障快速迭代器迭代集合时直接修改集合,则迭代器将抛出此异常。

解决方案

换Vector

两者区别:

  • ArrayList是线程不安全的,Vector是线程安全的。
  • 两者扩容方式不同。在底层数组容量不足时,ArrayList会将容量扩容为原来的1.5倍。而Vector支持在创建的时候主动声明扩容时增加的容量的大小,通过Vector(int initialCapacity, int capacityIncrement)构造函数实现。如果没有声明,或者capacityIncrement <= 0,那么默认扩容为原来的2倍.

// Vector的扩容方法

private void grow(int minCapacity) {

// overflow-conscious code

int oldCapacity = elementData.length;

int newCapacity = oldCapacity + ((capacityIncrement > 0) ?

capacityIncrement : oldCapacity);

if (newCapacity - minCapacity < 0)

newCapacity = minCapacity;

if (newCapacity - MAX_ARRAY_SIZE > 0)

newCapacity = hugeCapacity(minCapacity);

elementData = Arrays.copyOf(elementData, newCapacity);

}

验证Demo:

/**

* 验证Vector线程安全

*

* @author wangjie

* @version V1.0

* @date 2019/12/17

*/

public class ContainerSafe {

public static void main(String[] args) {

List list = new Vector<>();

for(int i = 0; i < 30 ; i++){

new Thread(() ->{

list.add(UUID.randomUUID().toString().substring(0,8));

System.out.println(list);

},i+"").start();

}

}

}

运行结果:

06bb44e3e87f700c4156727852021baf.png

用Collections.synchronizedList()

先上验证Demo:

public class ContainerSafe {

public static void main(String[] args) {

// List list = new Vector<>();

List list = Collections.synchronizedList(new ArrayList<>());

for(int i = 0; i < 30 ; i++){

new Thread(() ->{

list.add(UUID.randomUUID().toString().substring(0,8));

System.out.println(list);

},i+"").start();

}

}

}

运行结果:

5d1dea7a82e2f5894d1f8be6d06c798b.png

Vector和Collections.synchronizedList

虽然ArrayList是线程不安全的,但是通过Collections.synchronizedList()方法可以将线程不安全的List转成线程安全的List。但官方文档里,有这么一句话:

If you need synchronization, a Vector will be slightly faster than an ArrayList synchronized with Collections.synchronizedList.

Vector比Collections.synchronizedList快一点点。

下面是我扒的一部分源码:

public static List synchronizedList(List list) {

return (list instanceof RandomAccess ?

new SynchronizedRandomAccessList<>(list) :

new SynchronizedList<>(list));

}

SynchronizedList(List list, Object mutex) {

super(list, mutex);

this.list = list;

}

SynchronizedCollection(Collection c, Object mutex) {

this.c = Objects.requireNonNull(c);

this.mutex = Objects.requireNonNull(mutex);

}

public boolean add(E e) {

synchronized (mutex) {return c.add(e);}

}

}

从代码中可以看出,SynchronizedList类使用了委托(delegation),实质上存储还是使用了构造时传进来的list,只是将list作为底层存储,对它做了一层包装。正是因为多了一层封装,所以就会比直接操作数据的Vector慢那么一点点。

从上面的代码我们也可以看出来,SynchronizedList的同步,使用的是synchronized代码块对mutex对象加锁,这个mutex对象还能够通过构造函数传进来,也就是说我们可以指定锁定的对象。

而Vector则使用了synchronized方法,同步方法的作用范围是整个方法,所以没办法对同步进行细粒度的控制。而且同步方法加锁的是this对象,没办法控制锁定的对象。这也是vector和SynchronizedList的一个区别。

使用CopyOnWriteArrayList

什么是CopyOnWrite容器?

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

CopyOnWriteArrayList的实现原理,以下是我扒的CopyOnWriteArrayList的部分源码:

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 remove(int index) {

final ReentrantLock lock = this.lock;

lock.lock();

try {

Object[] elements = getArray();

int len = elements.length;

E oldValue = get(elements, index);

int numMoved = len - index - 1;

if (numMoved == 0)

setArray(Arrays.copyOf(elements, len - 1));

else {

Object[] newElements = new Object[len - 1];

System.arraycopy(elements, 0, newElements, 0, index);

System.arraycopy(elements, index + 1, newElements, index,

numMoved);

setArray(newElements);

}

return oldValue;

} finally {

lock.unlock();

}

}

public E set(int index, E element) {

final ReentrantLock lock = this.lock;

lock.lock();

try {

Object[] elements = getArray();

E oldValue = get(elements, index);

if (oldValue != element) {

int len = elements.length;

Object[] newElements = Arrays.copyOf(elements, len);

newElements[index] = element;

setArray(newElements);

} else {

// Not quite a no-op; ensures volatile write semantics

setArray(elements);

}

return oldValue;

} finally {

lock.unlock();

}

}

读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为开始读的那一刻已经确定了读的对象是旧对象。

写入和删除时加锁,所以一个线程X读取的时候另一个线程Y可能执行remove操作。remove操作首先要获取独占锁,然后进行写时复制操作,就是复制一份当前的array数组,然后在复制的新数组里面删除元素,删除完成后让array指向这个新的数组。

验证Demo:

public class ContainerSafe {

public static void main(String[] args) {

// List list = new Vector<>();

// List list = Collections.synchronizedList(new ArrayList<>());

List list = new CopyOnWriteArrayList<>();

for(int i = 0; i < 30 ; i++){

new Thread(() ->{

list.add(UUID.randomUUID().toString().substring(0,8));

System.out.println(list);

},i+"").start();

}

}

}

运行结果:

8789ff3cce9376410d3a47346c341a76.png

CopyOnWrite的缺点

CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

内存占用问题:因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。

针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。

数据一致性问题:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。【当执行add或remove操作没完成时,get获取的仍然是旧数组的元素】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值