java的各种集合为什么不安全(List、Set、Map)?

我们已经知道多线程下会有各种不安全的问题 , 都知道并发的基本解决方案 , 这里对出现错误的情况进行一个实际模拟 , 因此能够联想到具体的生产环境中 。
1|0一、List 的不安全1|11.1 问题看一段代码:
public static void main(String[] args) {ArrayList 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|21.2 产生原因普通的 ArrayList 集合里面没有任何特殊处理 , 在多线程情况下 , 他们可以共同进行访问 。
那么在多线程同时操作的时候 , 按照操作的情况就有这几种:

  1. 各个线程都读 。 不影响 , 前提是只有读;
  2. 各个线程都写 。 会出现问题 , 这里的点有两种情况:值覆盖问题 , 因为 ArrayList 的底层数组 , 写入值的时候要先计算到一个下标位置 , 然后给对应的位置去赋值 , 多线程就会出现值覆盖的问题;空指针异常 , 因为 ArrayList 的底层数组 , 写入值在数组满的时候需要扩容 , 在扩容还没完成的时候 , 新的下标却已经计算出来并且要去插入 , 那么就会出现空指针异常 。
  3. 有的读有的写 。 那么显然对于多个线程来说 , 2 里面各个线程写的情况对应的问题就会出现 。 除此之外:如果多线程有的读有的写 , 对于 ArrayList 底层 , 某些情况下 , 对象是不允许进行修改的 , 如果修改了 , 后面调用某些方法时 , 就会检测到 , 然后就直接抛出ConcurrentModificationException 。 具体一下 , 因为源码里 , 写操作对集合修改是写 , 而next、remove等 Itr 的遍历读操作的时候会通过当前集合的修改次数与 Itr 对象创建时记录的次数校验集合是否被修改 , 如果修改了 , 不一致就说明正读的时候还有别的线程在改 , 就会抛出异常 。 JDK作者说了 , 会抛这个异常的都叫fail-fast iterator 。
第 3 种情况就是对应了我们上面的代码在线程多起来的情况 , 因为输出 list 的时候需要遍历的读 , 而此时还有别的线程在进行 add 的修改操作 。
1|31.3 解决方法注意:当然不能自己加锁 , 因为集合类已经在演变过程有线程安全的替代品 , 自己的代码加锁的粒度已经在集合的外层再加一层了 , 粒度太大 。
  • 同样能够完成 ArrayList 功能的 , 可以使用 Vector , 查看源码就会发现 , Vector 的基本结构是一个叫 elementData 的 Object 类型的数组 , 和 ArrayList 类似 , 但是对应的操作方法 , 基本都加上了 synchronized 关键字 , 因此它是线程安全的集合 。
  • 数据量小的时候 , 使用 Collections.synchronizedList(new ArrayList())这种方式 , 来包裹这个集合 , 跟 Collections 里面 synchronizedMap包裹hashmap 是一样的 , 更多的 , 还有:

java的各种集合为什么不安全(List、Set、Map)?文章插图
显然能传入参数的这些基本集合类都是线程不安全的 。
  • 第三种就是 , 直接使用 juc 包里面的 , CopyOnWriteArrayList() 类 , 这个类就是并发包给我们提供的线程安全的列表类 。 1.4里介绍了这个集合 。
1|41.4 CopyOnWriteArrayList对于 CopyOnWriteArrayList 类 , 名字上就可以听的出来 , 写时复制的列表 。
首先 , 按照前面的我们的分析 , 只要涉及了写的操作 , 和读或者写搭配的多线程情况 , 就会出现问题 , 那么多线程同时读却不会出现问题 , 因此相比较于直接都加上 synchronized 的方式 , 他的思想就是:读写分离 。 这个思想在数据库对于高并发的架构层面也有一样的设计 。
这样一来 , 对于这个 List 集合来说 , 分为不同操作的保证线程安全的策略 , 就能够保证更好的性能 。
写的方法 , 我们首先可以看 add 方法源码:
java的各种集合为什么不安全(List、Set、Map)?文章插图