主要从以下几个方面介绍:
- ArrayList扩容机制
- ArrayList迭代器
- Spliterator多线程遍历
一.ArrayList扩容机制
ArrayList实现List接口,底层为一个可以调节长度的数组,主要成员变量如下。
1 //默认初始容量 2 private static final int DEFAULT_CAPACITY = 10; 3 4 //空数组的实例,供需要空数组的地方调用,有实意 5 private static final Object[] EMPTY_ELEMENTDATA = {}; 6 7 //同样是一个空数组的实例,不同的是,使用默认构造器返回这个数组 8 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 9 10 //存储数据 11 transient Object[] elementData; 12 13 //当前数组长度 14 private int size;
ArrayList有三种构造方法:
1.默认构造方法
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
可以看到返回一个空列表,elementData为一个空数组,长度为0,实际上第一次长度为10的扩容并不在此时完成,而是在第一次添加数据时。
2.构建一个包含指定集合的列表
1 public ArrayList(Collection<? extends E> c) { 2 elementData = c.toArray(); 3 if ((size = elementData.length) != 0) { 4 // c.toArray might (incorrectly) not return Object[] (see 6260652) 5 if (elementData.getClass() != Object[].class) 6 elementData = Arrays.copyOf(elementData, size, Object[].class); 7 } else { 8 // replace with empty array. 9 this.elementData = EMPTY_ELEMENTDATA; 10 } 11 }
3.构建一个指定初始容量的空列表
1 public ArrayList(int initialCapacity) { 2 if (initialCapacity > 0) { 3 this.elementData = new Object[initialCapacity]; 4 } else if (initialCapacity == 0) { 5 this.elementData = EMPTY_ELEMENTDATA; 6 } else { 7 throw new IllegalArgumentException("Illegal Capacity: "+ 8 initialCapacity); 9 } 10 }
接下来通过add()方法分析ArrayList的扩容机制。
1 public boolean add(E e) { 2 ensureCapacityInternal(size + 1); // Increments modCount!! 3 elementData[size++] = e; 4 return true; 5 }
可以看到add()方法只有两步,先扩容,然后添加元素,所以ensureCapacityInternal()为扩容的核心方法,接下来进入ensureCapacityInternal()。
1 private void ensureCapacityInternal(int minCapacity) { 2 ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); 3 } 4 5 private static int calculateCapacity(Object[] elementData, int minCapacity) { 6 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { 7 return Math.max(DEFAULT_CAPACITY, minCapacity); 8 } 9 return minCapacity; 10 } 11 12 private void ensureExplicitCapacity(int minCapacity) { 13 modCount++; 14 15 // overflow-conscious code 16 if (minCapacity - elementData.length > 0) 17 grow(minCapacity); 18 }
此处我调整了一下源码的顺序,可以更方便的看出方法之间的调用关系。其中minCapacity为添加元素后所需的容量,在calculateCapacity()方法里首先判断elementData是否为默认的空数组(即调用默认构造方法的结果),如果是,则返回DEFAULT_CAPACITY与minCapacity中较大的那个,DEFAULT_CAPACITY值为10,
然后调用ensureExplicitCapacity()方法,modCount+1,这个值记录当前ArrayList对象修改次数,主要用于快速失败的判断,后文会提到。然后比较列表新的容量与当前容量,如果当前容量小于新的容量,则进行具体的扩容操作,进入grow()方法。
1 private void grow(int minCapacity) { 2 // overflow-conscious code 3 int oldCapacity = elementData.length; 4 int newCapacity = oldCapacity + (oldCapacity >> 1); 5 if (newCapacity - minCapacity < 0) 6 newCapacity = minCapacity; 7 if (newCapacity - MAX_ARRAY_SIZE > 0) 8 newCapacity = hugeCapacity(minCapacity); 9 // minCapacity is usually close to size, so this is a win: 10 elementData = Arrays.copyOf(elementData, newCapacity); 11 }
可以看到新容量为旧容量的1.5倍(oldCapacity + (oldCapacity >> 1) 旧容量值做右移位运算,右移一位相当于除2,即1.5倍),如果扩充1.5倍后的新容量仍然小于所需要的容量,则直接让新容量等于所需容量,最后判断新容量长度是否超过最大长度,最后拷贝数据。
最后总结一下,使用默认构造方法返回的ArrayList对象首次添加单个元素时,列表长度扩容至10,若第一次是添加多个元素(addAll()方法等),扩容长度由添加的元素个数决定。
二.ArrayList迭代器
迭代器在开发中算是比较常用的东西,这部分主要想说一下ArrayList中的快速失败机制.
快速失败指的是在集合类的迭代过程中,集合内容被修改,如果被修改则抛出ConcurrentModificationException异常,产生fail-fast事件。
Iterator接口定义了以下几个方法
-
boolean hasNext()
-
E next()
-
void remove()
-
void forEachRemaining(Consumer<? super E> action)
在ArrayList中Itr实现了Iterator接口,源码如下:
1 private class Itr implements Iterator<E> { 2 int cursor; // 下一个元素的索引 3 int lastRet = -1; // 当前最后一次返回的元素的索引 4 int expectedModCount = modCount; //expectedModCount是快速失败机制中关键的锁,初始值当前列表对象的修改次数modCount 5 6 Itr() {} 7 8 public boolean hasNext() { 9 return cursor != size; 10 } 11 12 @SuppressWarnings("unchecked") 13 public E next() { 14 checkForComodification(); 15 int i = cursor; 16 if (i >= size) 17 throw new NoSuchElementException(); 18 Object[] elementData = ArrayList.this.elementData; 19 if (i >= elementData.length) 20 throw new ConcurrentModificationException(); 21 cursor = i + 1; 22 return (E) elementData[lastRet = i]; 23 } 24 25 public void remove() { 26 if (lastRet < 0) 27 throw new IllegalStateException(); 28 checkForComodification(); 29 30 try { 31 ArrayList.this.remove(lastRet); 32 cursor = lastRet; 33 lastRet = -1; 34 expectedModCount = modCount; 35 } catch (IndexOutOfBoundsException ex) { 36 throw new ConcurrentModificationException(); 37 } 38 } 39 40 @Override 41 @SuppressWarnings("unchecked") 42 public void forEachRemaining(Consumer<? super E> consumer) { 43 Objects.requireNonNull(consumer); 44 final int size = ArrayList.this.size; 45 int i = cursor; 46 if (i >= size) { 47 return; 48 } 49 final Object[] elementData = ArrayList.this.elementData; 50 if (i >= elementData.length) { 51 throw new ConcurrentModificationException(); 52 } 53 while (i != size && modCount == expectedModCount) { 54 consumer.accept((E) elementData[i++]); 55 } 56 // update once at end of iteration to reduce heap write traffic 57 cursor = i; 58 lastRet = i - 1; 59 checkForComodification(); 60 } 61 62 final void checkForComodification() { 63 if (modCount != expectedModCount) 64 throw new ConcurrentModificationException(); 65 } 66 }
在next()方法中,调用checkForComodification检查当前集合是否被其他线程修改过,可以看一下源码。
1 final void checkForComodification() { 2 if (modCount != expectedModCount) 3 throw new ConcurrentModificationException(); 4 }
可以看到,这里判断modCount与expectedModCount是否相等,如果不等,则说明在迭代过程中,可能有其他线程修改了集合结构或是迭代元素时调用了集合自身的新增或删除操作,集合不同步,抛出ConcurrentModificationException异常。
但是需要强调一点,快速失败机制只是一种错误检测机制,fail-fast事件并不是一定会产生,触发时机是在迭代过程中,集合的结构发生了变化而此时迭代器并不知道或者还没来得及反应时便会产生fail-fast事件,这种行为无法得到保证,因为ArrayList是线程不安全的,迭代器只会尽最大努力抛出 ConcurrentModificationException,所以在并发环境下,靠快速失败机制确保集合数据的一致性是不明智的。
PS. 解决办法:在多线程情况下遍历集合,使用“java.util.concurrent包下的类”去取代“java.util包下的类,比如将ArrayList替换为CopyOnWriteArrayList
,对于CopyOnWrite容器这里就不做过多介绍,中心思想就是,对一个集合进行操作时,不直接操作该集合,而是操作该集合的一个拷贝,操作结束后,将原容器的引用指向新容器,是一种读写分离的思想。
三.Spliterator
Spliterator是一个可分割的迭代器,JDK1.8中默认的数据结构都实现了Spliterator,主要目的是提高并行处理能力。
主要作用就是把集合分成了好几段,每个线程执行一段,因此是线程安全的。基于这个原理,以及modCount的快速失败机制,如果迭代过程中集合元素被修改,会抛出异常。
接口主要方法
1 //单个对元素执行给定的动作,如果有剩下元素未处理返回true,否则返回false 2 boolean tryAdvance(Consumer<? super T> action); 3 4 //对每个剩余元素执行给定的动作,依次处理,直到所有元素已被处理或被异常终止。默认方法调用tryAdvance方法 5 default void forEachRemaining(Consumer<? super T> action) { 6 do { } while (tryAdvance(action)); 7 } 8 9 //对任务分割,返回一个新的Spliterator迭代器 10 Spliterator<T> trySplit(); 11 12 //用于估算还剩下多少个元素需要遍历 13 long estimateSize(); 14 15 //当迭代器拥有SIZED特征时,返回剩余元素个数;否则返回-1 16 default long getExactSizeIfKnown() { 17 return (characteristics() & SIZED) == 0 ? -1L : estimateSize(); 18 } 19 20 //返回当前对象有哪些特征值 21 int characteristics(); 22 23 //是否具有当前特征值 24 default boolean hasCharacteristics(int characteristics) { 25 return (characteristics() & characteristics) == characteristics; 26 } 27 //如果Spliterator的list是通过Comparator排序的,则返回Comparator 28 //如果Spliterator的list是自然排序的 ,则返回null 29 //其他情况下抛错 30 default Comparator<? super T> getComparator() { 31 throw new IllegalStateException(); 32 }
针对这一部分为我设计了一个测试用例:创建长度为10的list,多线程求和。
public class ArrayListTest { static int count = 0; static List<Integer> list = getList(); static Spliterator spliterator = list.spliterator(); public static void main(String[] args) { for (int i = 0 ; i < 4; i++) new MyThread().start(); try { Thread.sleep(15000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(count); } static List<Integer> getList(){ List<Integer> list = new ArrayList<>(); for (int i = 1;i <= 10;i++ ){ list.add(i * 10); } return list; } static class MyThread extends Thread{ @Override public void run(){ String threadName = Thread.currentThread().getName(); System.out.println(threadName + "ing..."); spliterator.trySplit().forEachRemaining(x->{ System.out.println(x + threadName); count += (int)x; try { Thread.sleep(1500); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(threadName + "end!!!"); } } }
输出结果:
可以看到使用spliterator,多线程遍历各自的分段后集合进行求和运算,运算结果正确没有受到影响,
此时可以做一下对比,将spliterator.trySplit().forEachRemaining替换为list.forEach,可以看到输出结果:
没有截取完整的输出结果,但是可以直观的看到,四个线程各自对集合进行完整的遍历,结果为450*4=2200。
参考博客: