Java并发(五):并发,迭代器和容器
在随后的博文中我会继续分析并发包源码,在这里,得分别谈谈容器类和迭代器及其源码,虽然很突兀,但我认为这对于学习Java并发很重要;
ConcurrentModificationException:
JavaAPI中的解释:当不允许这样的修改时,可以通过检测到对象的并发修改的方法来抛出此异常。一个线程通常不允许修改集合,而另一个线程正在遍历它。 一般来说,在这种情况下,迭代的结果是未定义的。 某些迭代器实现(包括由JRE提供的所有通用集合实现的实现)可能会选择在检测到此行为时抛出此异常。 这样做的迭代器被称为"及时失败"迭代器,当他们发现容器在迭代时被修改时,就会报异常;它称不上时一种处理机制,而是一种预防机制,只能作为并发问题的预警指示器.
迭代器与容器:
Vector这个"古老"的容器类相信大家很熟悉了,他是线程安全的没错,我们利用elements()方法遍历,不会出现线程安全的问题:
1 /** 2 * JDK1.8源码 3 */ 4 5 /** 6 * Returns an enumeration of the components of this vector. The 7 * returned {@code Enumeration} object will generate all items in 8 * this vector. The first item generated is the item at index {@code 0}, 9 * then the item at index {@code 1}, and so on. 10 * 11 * @return an enumeration of the components of this vector 12 * @see Iterator 13 */ 14 public Enumeration<E> elements() { 15 return new Enumeration<E>() { 16 int count = 0; 17 18 public boolean hasMoreElements() { 19 return count < elementCount; 20 } 21 22 public E nextElement() { 23 synchronized (Vector.this) { 24 if (count < elementCount) { 25 return elementData(count++); 26 } 27 } 28 throw new NoSuchElementException("Vector Enumeration"); 29 } 30 }; 31 }
但当我们利用foreach进行迭代时,底层自动调用了iterator(),若我们不锁住容器,可能会报ConcurrentModificationException
1 /** 2 * Returns an iterator over the elements in this list in proper sequence. 3 * 4 * <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>. 5 * 6 * @return an iterator over the elements in this list in proper sequence 7 */ 8 public Iterator<E> iterator() { 9 return new Itr(); 10 //JDK1.8写成了一个内部类的形式返回迭代器 11 } 12 13 /** 14 * An optimized version of AbstractList.Itr 15 */ 16 private class Itr implements Iterator<E> { 17 int cursor; // index of next element to return 18 int lastRet = -1; // index of last element returned; -1 if no such 19 int expectedModCount = modCount; 20 21 Itr() {} 22 23 public boolean hasNext() { 24 return cursor != size; 25 } 26 27 //观察下面的代码可以发现,每次迭代一次都要检查容器长度是否改变 28 @SuppressWarnings("unchecked") 29 public E next() { 30 checkForComodification(); 31 int i = cursor; 32 if (i >= size) 33 throw new NoSuchElementException(); 34 Object[] elementData = ArrayList.this.elementData; 35 if (i >= elementData.length) 36 throw new ConcurrentModificationException(); 37 cursor = i + 1; 38 return (E) elementData[lastRet = i]; 39 } 40 41 public void remove() { 42 if (lastRet < 0) 43 throw new IllegalStateException(); 44 checkForComodification(); 45 46 try { 47 ArrayList.this.remove(lastRet); 48 cursor = lastRet; 49 lastRet = -1; 50 expectedModCount = modCount; 51 } catch (IndexOutOfBoundsException ex) { 52 throw new ConcurrentModificationException(); 53 } 54 } 55 56 @Override 57 @SuppressWarnings("unchecked") 58 public void forEachRemaining(Consumer<? super E> consumer) { 59 Objects.requireNonNull(consumer); 60 final int size = ArrayList.this.size; 61 int i = cursor; 62 if (i >= size) { 63 return; 64 } 65 final Object[] elementData = ArrayList.this.elementData; 66 if (i >= elementData.length) { 67 throw new ConcurrentModificationException(); 68 } 69 while (i != size && modCount == expectedModCount) { 70 consumer.accept((E) elementData[i++]); 71 } 72 // update once at end of iteration to reduce heap write traffic 73 cursor = i; 74 lastRet = i - 1; 75 checkForComodification(); 76 } 77 78 final void checkForComodification() { 79 if (modCount != expectedModCount) 80 throw new ConcurrentModificationException(); 81 } 82 }
查看代码时可以发现,iterator()的内部类中提供的next,remove等方法都进行了迭代操作,这样的迭代被称为"隐藏迭代器";
部分容器类的toString()方法会在调用StringBuilder的append()同时迭代容器,例如继承了AbstractSet的HashSet类,而AbstractSet又继承于AbstractCollection:
// String conversion /** * Returns a string representation of this collection. The string * representation consists of a list of the collection's elements in the * order they are returned by its iterator, enclosed in square brackets * (<tt>"[]"</tt>). Adjacent elements are separated by the characters * <tt>", "</tt> (comma and space). Elements are converted to strings as * by {@link String#valueOf(Object)}. * * @return a string representation of this collection */ public String toString() { Iterator<E> it = iterator(); if (! it.hasNext()) return "[]"; StringBuilder sb = new StringBuilder(); sb.append('['); for (;;) { E e = it.next(); sb.append(e == this ? "(this Collection)" : e); if (! it.hasNext()) return sb.append(']').toString(); sb.append(',').append(' '); } }
隐藏的迭代器普遍存在,甚至出现在hashCode,equals,contains,remove等方法中,此处不再赘述;
解决办法:
考虑到并发安全性,我们不得不对容器类单独加锁,但同时我们又不希望加锁,那样太损耗性能了,还存在线程饥饿和死锁的风险,极大降低吞吐量和CPU利用率;
作为有限的替代,我们可以考虑"克隆"容器,并在将其副本封闭起来进行迭代,避免了抛出ConcurrentModificationException,但在克隆过程中仍然要对容器加锁;显然,克隆容器存在系统开销,我们在选择这种方案时必须考虑的因素有:容器大小,每个元素上执行的工作,迭代操作相对于其它操作的调用频率,以及相应时间和系统吞吐率的需求,具体应用详见CopyOnWriteArrayList类.
参考材料:
Java并发编程实战