java的各种集合为什么不安全(List、Set、Map)以及代替方案

我们已经知道多线程下会有各种不安全的问题,都知道并发的基本解决方案,这里对出现错误的情况进行一个实际模拟,以此能够联想到具体的生产环境中。


一、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的各种集合为什么不安全(List、Set、Map)以及代替方案

那么,情况不严重的时候,这里显然还正常运行结束了,只是导致了还没来得及写的时候,就已经读出了数据。

如果把线程数增加试试,可能还会看到这样的奇观:

java的各种集合为什么不安全(List、Set、Map)以及代替方案

报错了:重点异常:java.util.ConcurrentModificationException,翻译过来就是并发修改异常

1.2 产生原因

普通的 ArrayList 集合里面没有任何特殊处理,在多线程情况下,他们可以共同进行访问。

那么在多线程同时操作的时候,按照操作的情况就有这几种:

各个线程都读。不影响,前提是只有读;

各个线程都写。会出现问题,这里的点有两种情况:

值覆盖问题,因为 ArrayList 的底层数组,写入值的时候要先计算到一个下标位置,然后给对应的位置去赋值,多线程就会出现值覆盖的问题;

空指针异常,因为 ArrayList 的底层数组,写入值在数组满的时候需要扩容,在扩容还没完成的时候,新的下标却已经计算出来并且要去插入,那么就会出现空指针异常。

有的读有的写。那么显然对于多个线程来说,2 里面各个线程写的情况对应的问题就会出现。除此之外:

如果多线程有的读有的写,对于 ArrayList 底层,某些情况下,对象是不允许进行修改的,如果修改了,后面调用某些方法时,就会检测到,然后就直接抛出ConcurrentModificationException。

具体一下,因为源码里,写操作对集合修改是写,而next、remove等 Itr 的遍历读操作的时候会通过当前集合的修改次数与 Itr 对象创建时记录的次数校验集合是否被修改,如果修改了,不一致就说明正读的时候还有别的线程在改,就会抛出异常。

JDK作者说了,会抛这个异常的都叫fail-fast iterator。

第 3 种情况就是对应了我们上面的代码在线程多起来的情况,因为输出 list 的时候需要遍历的读,而此时还有别的线程在进行 add 的修改操作。

1.3 解决方法

注意:当然不能自己加锁,因为集合类已经再演变过程有线程安全的替代品,自己的代码加锁的粒度已经在集合的外层再加一层了,粒度太大。

同样能够完成 ArrayList 功能的,可以使用 Vector,查看源码就会发现,Vector 的基本结构是一个叫 elementData 的 Object 类型的数组,和 ArrayList 类似,但是对应的操作方法,基本都加上了 synchronized 关键字,因此它是线程安全的集合。

数据量小的时候,使用 Collections.synchronizedList(new ArrayList())这种方式,来包裹这个集合,跟 Collections 里面 synchronizedMap包裹hashmap 是一样的,更多的,还有:

java的各种集合为什么不安全(List、Set、Map)以及代替方案

显然能传入参数的这些基本集合类都是线程不安全的。

第三种就是,直接使用 juc 包里面的,CopyOnWriteArrayList() 类,这个类就是并发包给我们提供的线程安全的列表类。1.4里介绍了这个集合。

1.4 CopyOnWriteArrayList

对于 CopyOnWriteArrayList 类,名字上就可以听的出来,写时复制的列表。

首先,按照前面的我们的分析,只要涉及了写的操作,和读或者写搭配的多线程情况,就会出现问题,那么多线程同时读却不会出现问题,因此相比较于直接都加上 synchronized 的方式,他的思想就是:读写分离。这个思想在数据库对于高并发的架构层面也有一样的设计。

这样一来,对于这个 List 集合来说,分为不同操作的保证线程安全的策略,就能够保证更好的性能。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpddjx.html