多线程环境下,不安全的容器有哪些?为什么?
publicvoidtest1(){List<String> list=newArrayList<>();for(int i=0; i<10; i++){newThread(()->{ list.add(UUID.randomUUID().toString().substring(0,5));System.out.println(list);},"线程"+ i).start();}}
运行后可能会抛出如下异常信息,专业术语叫做并发修改异常(java.util.ConcurrentModificationException)。此外,请注意ArrayList是不允许遍历时进行元素删除操作的。
[f5ae9, d9668, 3ee12, 0ded7] [f5ae9, d9668, 3ee12, 0ded7, 5a90e, 361b6, 0f49a] [f5ae9, d9668, 3ee12, 0ded7, 5a90e, 361b6] [f5ae9, d9668, 3ee12, 0ded7, 5a90e] [f5ae9, d9668, 3ee12, 0ded7, 5a90e, 361b6, 0f49a, a338e, aba8e, d8b70] [f5ae9, d9668, 3ee12, 0ded7, 5a90e, 361b6, 0f49a, a338e, aba8e] [f5ae9, d9668, 3ee12, 0ded7, 5a90e, 361b6, 0f49a, a338e] Exception in thread "线程1" Exception in thread "线程0" Exception in thread "线程3" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909) at java.util.ArrayList$Itr.next(ArrayList.java:859) at java.util.AbstractCollection.toString(AbstractCollection.java:461) at java.lang.String.valueOf(String.java:2994) at java.io.PrintStream.println(PrintStream.java:821) at cn.com.newdoone.test.UnSafeCollectionTest.lambda$test1$0(UnSafeCollectionTest.java:22) at java.lang.Thread.run(Thread.java:748)
解决方案
方案一:使用 Vector,Vector的add方法比List 多了synchronized 关键字
List<String> list=newVector<>();
方案二:使用 Collections.synchronized 转换为安全集合
List<String> list=Collections.synchronizedList(newArrayList<>());
方案三:使用 CopyOnWriteArrayList 。CopyOnWriteArrayList 采用的是 ReentrantLock 锁,这种方法叫做写入时复制,COW设计思想,计算机设计领域的一种优化策略。类似于读写锁中的写锁,读写分离,每个线程都能读,但写入数据的时候必须依次有序进行,避免写入覆盖,造成数据不一致的问题。
List<String> list=newCopyOnWriteArrayList<>();
总结
解决List的线程安全问题,采用 CopyWriteArrayList 解决。由于Vector采用的 synchronized 关键字,只要是有同步锁,那么性能肯定低下。
原理
CopyWriteArrayList 容器是即写时复制的容器,和ArrayList相比,优点是并发安全,缺点有两个:
多了内存的占用,写数据是copy一份完整的数据,单独进行操作,双份内存。
存在脏读的情况,数据写完之后,其它线程不一定是马上读到最新内容。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Je7ifgV6-1629535283306)(.\images\CopyOnWriteArrayList - 原理.png)]
publicvoidtest2(){Set<String> set=newHashSet<>();for(int i=0; i<10; i++){newThread(()->{ set.add(UUID.randomUUID().toString().substring(0,5));System.out.println(set);},"线程"+ i).start();}}
运行后抛出ConcurrentModificationException并发修改异常
解决方案
方案一:使用 HashTable。
方案二:使用 Collections.synchronized 。
方案三:使用 CopyOnWriteArraySet。
HashSet 的底层就是一个 HashMap,HashSet 的 add 方法实际上是去调用的 HashMap 的put方法,在HashMap 的 put 方法中,如果存入一个不重复的 key ,那么返回值为null,如果存入的key重复了,则返回上一个key对应的value值,所以在 HashSet 中判断 key 是否重复了,只需要判断HashMap的put方法的返回值是否为null。
publicHashSet(){ map=newHashMap<>();}// HashSet的add方法,不允许key重复的原因publicbooleanadd(E e){return map.put(e, PRESENT)==null;}
publicstaticvoidmain(String[] args){Map<String,Object> map=newHashMap<String,Object>(16,0.75f);for(int i=0; i<30; i++){newThread(()->{ map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));System.out.println(map);},"线程"+ i).start();}}
运行后抛出ConcurrentModificationException并发修改异常。
解决方案
方案一:使用 Collections.synchronized 转换为安全Map
Map<String,Object> map=Collections.synchronizedMap(newHashMap<>(16,0.75f));
方案二:使用 ConcurrentHashMap
Map<String,Object> map=newConcurrentHashMap<>();