一、ConcurrentModificationException异常

先看两段代码:

第一段(迭代时同时删除 非倒数第二 的元素):

public static void main(String[] args) {ArrayList<String> list=new ArrayList<String>();list.add("111");list.add("222");list.add("333");list.add("444");for(Iterator<String> iterator=list.iterator();iterator.hasNext();){String ele=iterator.next();if(ele.equals("111"))list.remove("222");}System.out.println(list);}

输出:

Exception in thread "main" java.util.ConcurrentModificationExceptionat java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:937)at java.base/java.util.ArrayList$Itr.next(ArrayList.java:891)at HelloWorld.main(HelloWorld.java:13)

第二段(迭代时同时删除 倒数第二 的元素):

public static void main(String[] args) {ArrayList<String> list=new ArrayList<String>();list.add("111");list.add("222");list.add("333");list.add("444");for(Iterator<String> iterator=list.iterator();iterator.hasNext();){String ele=iterator.next();if(ele.equals("333"))list.remove("222");}System.out.println(list);}

输出:

[111, 333, 444]

对于以上现象的解释如下(网上很多都是分析源码,让人很难理解,我也是一知半解,在此,我用最简单的语言来解释问题的根因):

第一段代码中,每次进行了next()方法时,源码会先判断modCount(集合的已修改次数)和expectedModCount(集合的预期修改次数)是否一致,初始化都是4。当第一个循环时,没有问题,进入分支执行remove,此时modCount就被加1了,与expectedModCount不同,报出异常。说白了就是不能让你做任何修改,包括add操作也是一样的报错。

第二段代码中,是比较巧的,当在倒数第二个元素执行remove后,它的size-1了,再到下次迭代时,与当前位置已经相同,认为已经遍历结束了,所以跳出迭代了,就没去执行next()方法了,因此没有异常了。

为什么它叫ConcurrentModificationException异常,即并发修改异常?

原因是它是为了防止多线程并发时导致这种不一致的发生(虽然可能是单线程的使用方法不当导致,即如以上代码)。这个也是集合的fast-fail(快速失败)机制。

二、安全集合类

1. 早期线程安全的集合

我们先从早期的线程安全的集合说起,它们是Vector和HashTable。

Vector

Vector和ArrayList类似,是长度可变的数组,与ArrayList不同的是,Vector是线程安全的,它给几乎所有的public方法都加上了synchronized关键字。由于加锁导致性能降低,在不需要并发访问同一对象时,这种强制性的同步机制就显得多余,所以现在Vector已被弃用。

HashTable

HashTable和HashMap类似,不同点是HashTable是线程安全的,它给几乎所有public方法都加上了synchronized关键字,还有一个不同点是HashTable的K,V都不能是null,但HashMap可以,它现在也因为性能原因被弃用了。

2. Collections包装方法

Vector和HashTable被弃用后,它们被ArrayList和HashMap代替,但它们不是线程安全的,所以Collections工具类中提供了相应的包装方法把它们包装成线程安全的集合:

List<E> synArrayList = Collections.synchronizedList(new ArrayList<E>());Set<E> synHashSet = Collections.synchronizedSet(new HashSet<E>());Map<K,V> synHashMap = Collections.synchronizedMap(new HashMap<K,V>());...

Collections针对每种集合都声明了一个线程安全的包装类,在原集合的基础上添加了锁对象,集合中的每个方法都通过这个锁对象实现同步。

3. java.util.concurrent包中的集合

ConcurrentHashMap

ConcurrentHashMap和HashTable都是线程安全的集合,它们的不同主要是加锁粒度上的不同。HashTable的加锁方法是给每个方法加上synchronized关键字,这样锁住的是整个Table对象。而ConcurrentHashMap是更细粒度的加锁。

在JDK1.8之前,ConcurrentHashMap加的是分段锁,也就是Segment锁,每个Segment含有整个table的一部分,这样不同分段之间的并发操作就互不影响。

JDK1.8对此做了进一步的改进,它取消了Segment字段,直接在table元素上加锁,实现对每一行进行加锁,进一步减小了并发冲突的概率。

CopyOnWriteArrayList 和 CopyOnWriteArraySet

它们是加了写锁的ArrayList和ArraySet,锁住的是整个对象,但读操作可以并发执行。

4. 其它Concurrent安全集合

除此之外还有ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue、ConcurrentLinkedDeque等,至于为什么没有ConcurrentArrayList,原因是无法设计一个通用的而且可以规避ArrayList的并发瓶颈的线程安全的集合类,只能锁住整个list,这用Collections里的包装类就能办到。

三、解决ConcurrentModificationException异常

现在重新回头来看第一部分异常的代码,此时我们就可以使用CopyOnWriteArrayList安全集合来解决了:

public static void main(String[] args) {List<String> list = new CopyOnWriteArrayList<String>();list.add("111");list.add("222");list.add("333");list.add("444");for(Iterator<String> iterator=list.iterator();iterator.hasNext();){String ele=iterator.next();if(ele.equals("111"))list.remove("222");}System.out.println(list);}

输出:

[111, 333, 444]

值得一提的是,使用Collections封装的安全类还是不能解决此问题:

List<String> list = Collections.synchronizedList(new ArrayList<String>());

输出:

Exception in thread "main" java.util.ConcurrentModificationExceptionat java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:937)at java.base/java.util.ArrayList$Itr.next(ArrayList.java:891)at HelloWorld.main(HelloWorld.java:13)

原文链接:

Java线程安全的集合详解_Java_长风-CSDN博客

如有错误,请更正指出,谢谢!

分类: 源码分享 标签: 暂无标签

评论

暂无评论数据

暂无评论数据

目录