在ArrayList,LinkedList,HashMap等等的内部实现增,删,改中咱们总能看到modCount的身影,modCount字面意思就是修改次数,但为何要记录modCount的修改次数呢?
你们发现一个公共特色没有,全部使用modCount属性的全是线程不安全的,这是为何呢?说明这个玩意确定和线程安全有关系喽,那有什么关系呢html
阅读源码,发现这玩意只有在本数据结构对应迭代器中才使用,以HashMap为例:java
private abstract class HashIterator<E> implements Iterator<E> { Entry<K,V> next; // next entry to return int expectedModCount; // For fast-fail int index; // current slot Entry<K,V> current; // current entry HashIterator() { expectedModCount = modCount; if (size > 0) { // advance to first entry Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; } } public final boolean hasNext() { return next != null; } final Entry<K,V> nextEntry() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); Entry<K,V> e = next; if (e == null) throw new NoSuchElementException(); if ((next = e.next) == null) { Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; } current = e; return e; } public void remove() { if (current == null) throw new IllegalStateException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); Object k = current.key; current = null; HashMap.this.removeEntryForKey(k); expectedModCount = modCount; } }
由以上代码能够看出,在一个迭代器初始的时候会赋予它调用这个迭代器的对象的mCount,如何在迭代器遍历的过程当中,一旦发现这个对象的mcount和迭代器中存储的mcount不同那就抛异常
好的,下面是这个的完整解释
Fail-Fast 机制
咱们知道 java.util.HashMap 不是线程安全的,所以若是在使用迭代器的过程当中有其余线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。这一策略在源码中的实现是经过 modCount 域,modCount 顾名思义就是修改次数,对HashMap 内容的修改都将增长这个值,那么在迭代器初始化过程当中会将这个值赋给迭代器的 expectedModCount。在迭代过程当中,判断 modCount 跟 expectedModCount 是否相等,若是不相等就表示已经有其余线程修改了 Map:注意到 modCount 声明为 volatile,保证线程之间修改的可见性。数组
因此在这里和你们建议,当你们遍历那些非线程安全的数据结构时,尽可能使用迭代器安全
我在看HashMap源码的时候发现了一个没思考过的问题,在这以前能够说是彻底没有思考过这个问题,在一开始对这个点有疑问的时候也没有想到竟然有这么个语法细节存在,弄得我百思不得其解,直到本身动手作实验改写了代码才彻底明白。数据结构
HashMap里面保存的数据最底层是一个Entry型的数组,这个Entry则保留了一个键值对,还有一个指向下一个Entry的指针。因此HashMap是一种结合了数组和链表的结构。正由于如此,你有3种对数据的观测方式:keySet,values,entrySet。第一个是体现从key的值角度出发的结果。它里面包含了这个键值对表里面的全部键的值的集合,由于HashMap明确规定一个键只能对应一个值,因此不会有重复的key存在,这也就是为何能够用集合来装key。第二个values则是从键值对的值的角度看这个映射表,由于能够有多个key对应一个值,因此可能有多个相同的values。(这个观点和函数的观点类似)第三个角度是最基本的角度,也就是从键值对的角度思考这个问题。它返回一个键值对的集合。(键值对相等当且仅当键和值都相等)。ide
以上是大体的理解。在此基础上,java的源码:(这里我只用了keySet说明这个问题)函数
1 public Set<K> keySet() { 2 Set<K> ks = keySet; 3 return (ks != null ? ks : (keySet = new KeySet())); 4 } 5 6 private final class KeySet extends AbstractSet<K> { 7 public Iterator<K> iterator() { 8 return newKeyIterator(); 9 } 10 public int size() { 11 return size; 12 } 13 public boolean contains(Object o) { 14 return containsKey(o); 15 } 16 public boolean remove(Object o) { 17 return HashMap.this.removeEntryForKey(o) != null; 18 } 19 public void clear() { 20 HashMap.this.clear(); 21 } 22 }
看上去简单明了,但是我发现了一个细节而且与之纠缠了一个下午(这个语法细节隐藏的很深)。this
这个地方咱们能够看到,当向一个HashMap调用keySet()方法的时候就是返回一个集合,其内容是全部的key的值。但是问题是这个地方究竟是怎么实现的。从代码能够看到这个地方直接返回了一个叫keySet的东西。那么这个东西到底是什么呢?按住command键能够直接去看这个变量声明的地方:spa
在AbstractMap.class里面:线程
1 transient volatile Set<K> keySet = null; 2 transient volatile Collection<V> values = null;
也就是说,这个地方是从HashMap的父类AbstractMap里面继承过来的两个集合类型(第一个就是我说的keySet,第二个和这个彻底同样的过程)。
但是问题仍是没有解决,这个keySet为何能返回当前HashMap的key的值得集合呢?我一开始只是抱着“简单看看”的想法来看这个地方,由于个人想象是可能能在哪里找到一个显而易见的同步方法,使得keySet的里面的值随着table(这也就是那个基础数组,储存了全部的键值对Entry)的值变化而变化。但是我发现:“没有”。
第一时间我以为我可能没有找对位置,由于通常它提供的这些类的继承关系比较复杂,可能不在这个地方,可能在别的地方实现了,但是我翻来覆去找半天确实发现没有,也就是说:“没有明确的代码让keySet同步HashMap”。这下问题就变大了,事实上若是你在AbstractMap里面找只找获得以下代码:
1 public Set<K> keySet() { 2 if (keySet == null) { 3 keySet = new AbstractSet<K>() { 4 public Iterator<K> iterator() { 5 return new Iterator<K>() { 6 private Iterator<Entry<K,V>> i = entrySet().iterator(); 7 8 public boolean hasNext() { 9 return i.hasNext(); 10 } 11 12 public K next() { 13 return i.next().getKey(); 14 } 15 16 public void remove() { 17 i.remove(); 18 } 19 }; 20 } 21 22 public int size() { 23 return AbstractMap.this.size(); 24 } 25 26 public boolean isEmpty() { 27 return AbstractMap.this.isEmpty(); 28 } 29 30 public void clear() { 31 AbstractMap.this.clear(); 32 } 33 34 public boolean contains(Object k) { 35 return AbstractMap.this.containsKey(k); 36 } 37 }; 38 } 39 return keySet; 40 }
看上去彻底不是一个同步过程,至少在个人理解中把一个容器的东西搬运到另一个容器须要用循环把东西一个一个搬运过去,哪怕只是浅拷贝把指针的值丢过去。这一节代码怎么看都和“让keySet这个set持有table里面的key的值的集合”没有任何关系。可是确确实实是这个地方实现了同步。
看以下代码:
1 public class Main { 2 3 public static void main(String[] args) { 4 5 testIterator t = new testIterator(); 6 Set<Integer> set = t.keySet(); 7 System.out.println(set); 8 9 } 10 } 11 12 13 class testIterator { 14 public Set<Integer> keySet() { 15 16 final ArrayList<Integer> result = new ArrayList<Integer>(); 17 result.add(1); 18 result.add(2); 19 result.add(3); 20 21 Set<Integer> keySet = new AbstractSet<Integer>() { 22 public Iterator<Integer> iterator() { 23 return new Iterator<Integer>() { 24 private Iterator<Integer> i = result.iterator(); 25 26 @Override 27 public boolean hasNext() { 28 return i.hasNext(); 29 } 30 31 @Override 32 public Integer next() { 33 return i.next(); 34 } 35 36 @Override 37 public void remove() { 38 i.remove(); 39 } 40 }; 41 } 42 43 @Override 44 public int size() { 45 return 0; 46 } 47 }; 48 49 return keySet; 50 } 51 }
这个地方的结果是:
[1, 2, 3]
为何呢?这个地方的代码是按照HashMap的代码改写的,我再改写一下以下所示:
1 public class Main { 2 3 public static void main(String[] args) { 4 ArrayList<Integer> array = new ArrayList<Integer>(); 5 array.add(1); 6 array.add(2); 7 array.add(3); 8 9 mySet set = new mySet(array.iterator()); 10 System.out.println(set); 11 } 12 13 } 14 15 class mySet extends AbstractSet<Integer> { 16 17 private Iterator<Integer> iter; 18 19 public mySet(Iterator<Integer> i) { 20 iter = i; 21 } 22 23 @Override 24 public Iterator<Integer> iterator() { 25 return iter; 26 } 27 28 @Override 29 public int size() { 30 return 0; 31 } 32 33 }
也是同样的效果。换句话说,直接让一个set它持有一个别人的Iterrator,它会认为本身是它。同时若是调试运行会发现set的值真的变了。同时这么作是有问题的,调试运行的结果和直接运行不同同时再加上一句:System.out.println(set); 会发现第一次打印了1,2,3,第二次为null。换句话说这样的代码产生了不肯定的行为。可是这代码能够说明一些问题,至少表示离问题近了。
到目前为止,能够知道keySet返回的并非个“新”的东西,因此也没有把HashMap里面的key的值一个一个放到set的这个过程,而是经过生成一个set,这个set直接和HashMap的Iterator挂钩来反映HashMap的变化。这个地方的“挂钩”的具体过程是keySet继承了AbstractSet这个抽象类,这个抽象类须要重写iterator() 方法。
具体的代码调用过程以下:
当你调用HashMap的keySet()方法的时候:
1 public Set<K> keySet() { 2 Set<K> ks = keySet; 3 return (ks != null ? ks : (keySet = new KeySet())); 4 } 5 6 private final class KeySet extends AbstractSet<K> { 7 public Iterator<K> iterator() { 8 return newKeyIterator(); 9 } 10 public int size() { 11 return size; 12 } 13 public boolean contains(Object o) { 14 return containsKey(o); 15 } 16 public boolean remove(Object o) { 17 return HashMap.this.removeEntryForKey(o) != null; 18 } 19 public void clear() { 20 HashMap.this.clear(); 21 } 22 }
可见:会返回一个名字叫keySet的Set。可是这个keySet如上面所写的是来自AbstractMap的一个引用。我前面思路错的缘由是由于我一直认为须要去AbstractMap里面找它的具体实现,其实不是的。这个ks的第一次初始化就反映了问题的本质是经过引用。看它的初始化过程:返回了一个“newKeyIterator();”对象。那么这个对象是什么呢?
再往前的代码:
1 Iterator<K> newKeyIterator() { 2 return new KeyIterator(); 3 }
它调用了一个方法返回了一个 KeyIterator 对象。这个对象的代码如图所示:
1 private final class KeyIterator extends HashIterator<K> { 2 public K next() { 3 return nextEntry().getKey(); 4 } 5 }
它又基础自HashIterator。看上去这个过程比较复杂,其实看源代码的话能够很清楚它的意图:keySet和values和entrySet本质既然同样,就能够经过封装其相同的部分(也就是这里的HashIterator),再各自实现最重要的next方法。
这是HashIterator的源代码:
1 private abstract class HashIterator<E> implements Iterator<E> { 2 Entry<K,V> next; // next entry to return 3 int expectedModCount; // For fast-fail 4 int index; // current slot 5 Entry<K,V> current; // current entry 6 7 HashIterator() { 8 expectedModCount = modCount; 9 if (size > 0) { // advance to first entry 10 Entry[] t = table; 11 while (index < t.length && (next = t[index++]) == null) 12 ; 13 } 14 } 15 16 public final boolean hasNext() { 17 return next != null; 18 } 19 20 final Entry<K,V> nextEntry() { 21 if (modCount != expectedModCount) 22 throw new ConcurrentModificationException(); 23 Entry<K,V> e = next; 24 if (e == null) 25 throw new NoSuchElementException(); 26 27 if ((next = e.next) == null) { 28 Entry[] t = table; 29 while (index < t.length && (next = t[index++]) == null) 30 ; 31 } 32 current = e; 33 return e; 34 }
可见,对于迭代器的操做,其实都是根据底层的table来实现的,也就是直接操做键值对。在获得Entry以后再得到它的key或者value。正由于如此,迭代器的底层直接根据table进行操做,因此若是有别的容器持有了这个迭代器内部类,就能够直接实现同步中的可见性:对HashMap的改变体如今table,而传递出去的内部类能够访问table。
而这之因此能够实现的更底层一步的地方是迭代器的具体实现。一方面它是一个内部类能够直接访问HashMap的table,另一个方面是它用了相似指针的next引用,也就能够实现迭代。这种暴露一个内部类来实现外部访问的方式我还真是第一次具体见到。
到这里咱们就能够明白这整个过程了。