为什么要保证数据变更可见性?
这个问题我们可以通过并发编程三要素中的可见性来说,它通常指一个线程对系统内共享变量的修改可以被其他线程感知的特性,如果无法保证修改行为的可见,那么其他线程就无法正确的执行下去。
看看源码?
我们以HashMap为例:
其中,会有两个关键的属性,modCount和expectedModCount;
而ConcurrentModificationException(并发修改异常),这个涉及到 fast-fail 机制(快速失败),可以提前预料遍历失败情况,防止数组越界异常;
/** The common parts of next() across different types of iterators */
protected Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (nextKey == null && !hasNext())
throw new NoSuchElementException();
lastReturned = entry;
entry = entry.next;
currentKey = nextKey;
nextKey = null;
return lastReturned;
}
这两个属性,就是保证线程不安全集合数据变更可见性的关键。
这两个属性的说明感兴趣的可以翻一下ConcurrentModificationException类中的说明。
大意是这个异常触发于集合不允许修改的情况下对对象进行修改,它也举例了实际场景,当一个线程正在迭代遍历集合,而另一个线程对该集合进行了迭代。而这些迭代是不可被感知的未定义的迭代,所以某些迭代器(集合的遍历)可以选择抛出此异常,而这些迭代器被成为故障快速迭代器( fail-fast iterator)。它还说明了如果一个线程在遍历的同时迭代,也会抛出这个异常,典型的例子就是集合在for循环的同时remove元素。
modCount干什么用?
我们上面知道了快速失败的基本内容,那么如何实现的呢?
在源码的记载中,modCount表示集合被修改(不一定是增删改都符合)的次数,所以初始值为0,而expectedModCount在初始化时值等于modCount。
各个集合中的增删改方法中可选的追加了modCount值变更。
举个例子:
HashMap<Integer,Integer> map = new HashMap<>();
map.put(1,1);
map.put(2,1);
map.put(3,1);
for (Integer key:
map.keySet()) {
//map.remove(key);会触发异常
//map.put(key,444);不会触发异常
map.put(999,444);
System.out.println(key);
}
当运行这段代码时,你就会得到ConcurrentModificationException;
因为HashMap在put新元素以及remove中都有调用对ModCount的改变。
我们试试ArrayList呢?
ArrayList<Integer> list=new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
for(Integer i:list){
//list.remove(1);
list.add(1);
System.out.println(i);
}
同样,remove和add方法都会抛出异常。但本身元素值的修改并不会触发该异常。
private static List<Integer> LIST = new ArrayList<Integer>(){
{
add(1);
add(2);
add(3);
}
};
public static void main(String[] args) {
new Thread(() -> {
Iterator<Integer> iterator = LIST.iterator();
while (iterator.hasNext()) {
iterator.next();
iterator.remove();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
LIST.set(0,1);
LIST.add(999);
}).start();
}
总结
集合通过快速失败异常来保证集合数据变更的可见性,但并不完全可靠,对同一个值的修改(set)不会触发异常。而删除(add)和新增(remove)会报错。