时间:2021-05-19
我们已经知道多线程下会有各种不安全的问题,都知道并发的基本解决方案,这里对出现错误的情况进行一个实际模拟,以此能够联想到具体的生产环境中。
一、List 的不安全
1.1 问题
看一段代码:
public static void main(String[] args) { ArrayList<String> list = new ArrayList<>(); for (int i = 0; i < 3; i++){ new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,8)); System.out.println(list); },String.valueOf(i)).start(); }}过程很简单,只有 3 个线程而已,对同一个 list 进行 add 的写操作,并随后进行输出的读操作。
输出结果,多执行几次,惊喜多多。
那么,情况不严重的时候,这里显然还正常运行结束了,只是导致了还没来得及写的时候,就已经读出了数据。
如果把线程数增加试试,可能还会看到这样的奇观:
报错了:重点异常:java.util.ConcurrentModificationException,翻译过来就是并发修改异常。
1.2 产生原因
普通的 ArrayList 集合里面没有任何特殊处理,在多线程情况下,他们可以共同进行访问。
那么在多线程同时操作的时候,按照操作的情况就有这几种:
各个线程都读。不影响,前提是只有读;
各个线程都写。会出现问题,这里的点有两种情况:
有的读有的写。那么显然对于多个线程来说,2 里面各个线程写的情况对应的问题就会出现。除此之外:
第 3 种情况就是对应了我们上面的代码在线程多起来的情况,因为输出 list 的时候需要遍历的读,而此时还有别的线程在进行 add 的修改操作。
1.3 解决方法
注意:当然不能自己加锁,因为集合类已经再演变过程有线程安全的替代品,自己的代码加锁的粒度已经在集合的外层再加一层了,粒度太大。
显然能传入参数的这些基本集合类都是线程不安全的。
第三种就是,直接使用 juc 包里面的,CopyOnWriteArrayList() 类,这个类就是并发包给我们提供的线程安全的列表类。1.4里介绍了这个集合。
1.4 CopyOnWriteArrayList
对于 CopyOnWriteArrayList 类,名字上就可以听的出来,写时复制的列表。
首先,按照前面的我们的分析,只要涉及了写的操作,和读或者写搭配的多线程情况,就会出现问题,那么多线程同时读却不会出现问题,因此相比较于直接都加上 synchronized 的方式,他的思想就是:读写分离。这个思想在数据库对于高并发的架构层面也有一样的设计。
这样一来,对于这个 List 集合来说,分为不同操作的保证线程安全的策略,就能够保证更好的性能。
写的方法,我们首先可以看 add 方法源码:
步骤很清楚,如果有了写操作,需要加锁:
其中的 lock 在源码里就是一个:
可以看到是一个普通的 Object。
那么加锁的时候就用 synchronized 对 Object 进行加锁,没有采用 juc 的 ReetrantLock,注释li也写了,偏向于使用内置的 monitor 也就是 synchronized 底层 monitor 锁,这一点也充分说明了 synchronized 的性能更新使得源码作者使用它。
这个方法是处理最直接的,其他对应的写操作:remove、set等等也是一样的基础流程。
我们再来看看读操作 get 方法:
二、HashSet 的不安全
2.1 问题及原因
我们还是用 List 一样的测试代码;
public class TestSet { public static void main(String[] args) { HashSet<String> set = new HashSet<>(); for (int i = 0; i < 100; i++){ new Thread(()->{ set.add(UUID.randomUUID().toString().substring(0,8)); System.out.println(set); },String.valueOf(i)).start(); } }}就会看到一样的错误:
2.2 出现问题的原因
其实从出现 ConcurrentModificationException 异常来看,我们可以猜测是和 List 类似的原因导致的异常。
可以看到,源码里面,Set 的底层维护的是一个 HashMap 来实现。对于遍历操作来说,都是一样的使用了 fail-fast iterator 迭代器,因此会出现这个异常。
另外,因为 HashSet 的底层是 HashMap ,本质上,对于每一个 key ,保证唯一,使用了一个 value 为 PRESENT 常量的键值对进行存储。
put 的过程也是调用 map 的 put 方法。
2.3 解决方案
2.4 CopyOnWriteArraySet
按照前面的思路,List 的对应线程安全集合是在 List 集合的数组基础上进行加锁的相关操作。
那么 Set 既然底层是 HashMap,对应的线程安全集合就应该是对 HashMap 的线程安全集合进行加锁,或者说直接用 ConcurrentHashMap 集合来实现 CopyOnWriteArraySet 。
但事实上,源码并不是这么做的。
从名字来看,和 ConcurrentHashMap 也没有什么关系,而是类似 CopyOnWriteArrayList 的命名,说明是读写单独处理,来让他成为线程安全的集合,那为什么是 ArraySet 多一个 array 修饰语呢?
可以看到,他的思路没有顺延 util 包的 HashSet 的实现思路,而是直接使用了 CopyOnWriteArrayList 作为底层数据结构。也就是说没有利用 Map 的键值对映射的特性来保证 set 的唯一性,而是用一个数组为基底的列表来实现。(那显然在去重方面就要做额外的操作了。)
然后每一个实现的方法都很简单,基本是直接调用了 CopyOnWriteArrayList 的方法:
我们最担心的可能 产生问题的 remove 和 add 方法,也是使用了 CopyOnWriteArrayList 的方法:
而保证 set 的不重复性质的关键,显然就在于 CopyOnWriteArrayList 的 addIfAbsent 方法,我们还是点进 CopyOnWriteArrayList 源码看一看这个方法的实现:
其中的 indexOfRange 方法:
可以看到,也是加了 Monitor 锁来进行的,整个过程是这样的:
总结一下就是,线程安全的 Set 集合完全利用了 CopyOnWriteArrayList 集合的方法,对应的操作也是读写分别处理,写时复制的策略,通过 jvm 层面的锁来保证安全,那么保证不重复的方法就是遍历进行比较。
这样看来,相比于基于 HashMap 的去重方法,效率肯定会降低,不过如果基于线程安全的 HashMap ,插入操作从hash、比较、到考虑扩容各方面会因为加锁的过程更复杂,而对于一个不重复的 Set 来说,完全没必要,所以应该综合考虑之下采用了 List 为基础,暴力循环去重。
三、HashMap 的线程不安全
关于 HashMap 的相关问题,源码里已经分析过,大体是这样的。
不安全:
解决:
HashMap 和 ConcurrentHashMap 的源码分析:
HashMap源码解析、jdk7和8之后的区别、相关问题分析
ConcurrentHashMap源码解析,多线程扩容
到此这篇关于java的各种集合为什么不安全(List、Set、Map)以及代替方案的文章就介绍到这了,更多相关java 集合不安全内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!
声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。
java有三种集合,包括Set、List和Map,它们都处于java.util包中,Set、List和Map都是接口,它们有各自的实现类。 1、Set中的对象
Java集合包Java集合主体内容包括Collection集合和Map类;而Collection集合又可以划分为List(队列)和Set(集合)。1.List的
Java中List、Set集合及Map的使用代码如下所示:packagetingjizifu;importjava.util.*;publicclassTong
Java中TreeSet是Set的一个子类。Set、List、Map区别Set是一个无序、不允许重复的集合。List(ArrayList、Vector等)是有序
详解Java中list,set,map的遍历与增强for循环Java集合类可分为三大块,分别是从Collection接口延伸出的List、Set和以键值对形式作