ArrayList/Vector的原理、线程安全和迭代Fail-Fast
疑问
* ArrayList是非线程非安全的,具体是指什么?具体会产生什么问题?
* ArrayList的内部原理是什么?为什么可以动态扩容?
* Vector是线程安全的,具体是如何实现的?为什么不再推荐使用?还有它的适用场景吗?
* 迭代时集合发生了修改怎么办?什么是fail-fast?
线程安全和非线程安全
Vector内部是如何实现线程安全的?
public class Vector { Object[] elementData; // 存放元素的数组 int elementCount; // 存放元素的实际数量,默认的容量(capacity)是10 int capacityIncrement; // 当容量占满时,扩容量,如果未指定,则原先的2倍(doubled) // 构造函数 public Vector(int initialCapacity/* 初始容量 */,int capacityIncrement/*扩容量*/){} }
其 capacity()/size()/isEmpty()/indexOf()/lastIndexOf()/removeElement()/addElement() 等方法均是 sychronized 的,所以,对Vector的操作均是线程安全的。
Vector是线程安全的,有问题吗?
如果用户知道自己是在单线程情况下运行,那么Vector本身的线程安全就没有必要了,耗费性能。JDK至少要提供一种非线程安全的List,供用户在不同的场景中选择,由此ArrayList出现了。ArrayList虽然是非线程安全的,但如果你想使用线程安全的ArrayList,可以在ArrayList的基础上,通过同步块来实现,或者使用同步包装器(Collections.synchronizedList),还可以使用J.U.C中的CopyOnWriteArrayList。但对于Vector,在其基础之上没有办法获得非线程安全的Vector(无法解耦)。这说明,在设计Vector时,没有做好分离性(数据结构功能和同步功能的分离)。
ArrayList的非线程安全会有什么问题?
Demo
final ArrayList<String> list = new ArrayList<String>(); // 多线程共享的ArrayList for(int i=0;i<100;i++) // 多个线程同时进行写操作 { new Thread(new Runnable(){ @Override public void run() { for(int j=0;j<1000;j++) { list.add("hello"); // 多线程下,此处引发ArrayIndexOutOfBoundsException } }}).start(); }
ArrayList的内部原理
public class ArrayList<E> { private Object[] elementData; // 存储元素的数组。其分配的空间长度是capacity。 private int size; // elementData存储了多少个元素。 public ArrayList(){this(10);}; // 默认capacity是10 boolean add(E e) { ensureCapacityInternal(size + 1); // capacity至少为 size+1 elementsData[size++]=e; // size++ return true; } void ensureCapacityInternal(int minCapacity){ if(minCapacity > elementData.length) // 扩容 grow(minCapacity); } void grow(int minCapacity){ int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // 约是原先的1.5倍。 elementData = Arrays.copyOf(elementData,newCapacity ); } }
如何实现线程安全的ArrayList?
#1:自己手动同步
public static List<E> list = ... ;
lock.lock();
list.add();
lock.unlock();
#2:使用同步包装器
List<E> syncList = Collections.synchronizedList(new ArrayList<E>());
迭代时,需要包含在同步块当中
synchronized(syncList){
while(Iterator<E> iter = syncList.iterator();iter.hasNext();){}
}
#3:使用J.U.C中的CopyOnWriteArrayList。
迭代 / Fail-fast
什么是fail-fast?
一个fail-fast的系统是指当发现任何可能导致过程失败的情况时,立刻抛出错误。一个fail-fast的迭代器是指当迭代的集合正在迭代时检测到集合发生了修改,就立刻抛出一个异常。
ArrayList的fail-falst的实现
public class ArrayList { protected transient int modCount = 0; // 用来记录修改次数(继承自AbstractList) add() / remove() / trimToSize() / ensureCapacity() ... { modCount++; // 每次修改,modCount均自增 } class Itr implements Iterator<E> { int expectedModCount = modCount; // 记录modeCount当前值(一次快照) public E next() { checkForComodification(); // next()操作之前,check一次 ... } public void remove(){ checkForComodification(); // remove()操作之前,check一次 ... ArrayList.this.remove(lastRet); ...
// 更新modCount的快照
// 这说明通过iter的Remove()来删除元素不会抛出ConcurrentModificationException expectedModCount = modCount; } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } } }
迭代时如何修改ArrayList?
Iterator<String> iter = list.iterator(); int j=0; while(iter.hasNext()) { System.out.println(iter.next()); if(j==3) { list.remove(0); // 出现 ConcurrentModificationException。 iter.remove(); // (单线程下)不会引发ConcurrentModificationException。但迭代器也只有这个修改相关的操作。 } j++; }
ConcurrentModificationException 这个异常看起来像是“多线程并发修改异常”,其实单线程下的迭代时修改也可能会出现这个异常。单线程下,迭代时通过集合自身的操作修改集合,会引发异常;通过迭代器修改(即 iter.remove() )不会引发 ConcurrentModificationException 。多线程下,迭代时通过迭代器修改可能会引发 ConcurrentModificationException ,此时应该使用线程安全的集合。