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.util.ConcurrentModificationException,翻译过来就是并发修改异常。

1.2 产生原因

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

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

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

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

  1. 值覆盖问题,因为 ArrayList 的底层数组,写入值的时候要先计算到一个下标位置,然后给对应的位置去赋值,多线程就会出现值覆盖的问题;
  2. 空指针异常,因为 ArrayList 的底层数组,写入值在数组满的时候需要扩容,在扩容还没完成的时候,新的下标却已经计算出来并且要去插入,那么就会出现空指针异常。

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

  1. 如果多线程有的读有的写,对于 ArrayList 底层,某些情况下,对象是不允许进行修改的,如果修改了,后面调用某些方法时,就会检测到,然后就直接抛出ConcurrentModificationException。
  2. 具体一下,因为源码里,写操作对集合修改是写,而next、remove等 Itr 的遍历读操作的时候会通过当前集合的修改次数与 Itr 对象创建时记录的次数校验集合是否被修改,如果修改了,不一致就说明正读的时候还有别的线程在改,就会抛出异常。
  3. JDK作者说了,会抛这个异常的都叫fail-fast iterator。

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

1.3 解决方法

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

  • 同样能够完成 ArrayList 功能的,可以使用 Vector,查看源码就会发现,Vector 的基本结构是一个叫 elementData 的 Object 类型的数组,和 ArrayList 类似,但是对应的操作方法,基本都加上了 synchronized 关键字,因此它是线程安全的集合。
  • 数据量小的时候,使用 Collections.synchronizedList(new ArrayList())这种方式,来包裹这个集合,跟 Collections 里面 synchronizedMap包裹hashmap 是一样的,更多的,还有:

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

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

1.4 CopyOnWriteArrayList

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

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

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

写的方法,我们首先可以看 add 方法源码:

步骤很清楚,如果有了写操作,需要加锁:

  • 加锁
  • 获取到当前的集合数组;
  • 计算长度;
  • 调用 Arrays.copyOf 方法进行添加操作,每次只添加一个元素进去;
  • 修改引用,更新最新的集合;
  • return true。
  • 解锁

其中的 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 解决方案

  • List 有对应的 Vector 可用,本来就是线程安全的集合,但是 Set 没有;
  • 数据量小的时候,使用 Collections.synchronizedSet(new HashSet<>()) 这种方式,来包裹这个集合,上面我们使用 List 的时候也有类似的方法;
  • 同样的,juc包为我们提供了新的线程安全集合 CopyOnWriteArraySet()。

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 ,是一个数组,以快照形式返回当前的数组;
  • indexOfRange 方法通过遍历查找查找元素出现位置,addIfAbsent方法完成不存在则加入,如果前一个为 false 后一个就不会执行;
  • 加锁;
  • current 再次获取一次当前的快照,因为有可能第一次判断的过程有了其他线程的插入或者修改操作,此时已经不像等,就进入分支进行判断是否存在;
  • 否则就要加入这个元素,和 CopyOnWriteArrayList 添加元素的最后操作是一样的;
  • 解锁。

总结一下就是,线程安全的 Set 集合完全利用了 CopyOnWriteArrayList 集合的方法,对应的操作也是读写分别处理,写时复制的策略,通过 jvm 层面的锁来保证安全,那么保证不重复的方法就是遍历进行比较。

这样看来,相比于基于 HashMap 的去重方法,效率肯定会降低,不过如果基于线程安全的 HashMap ,插入操作从hash、比较、到考虑扩容各方面会因为加锁的过程更复杂,而对于一个不重复的 Set 来说,完全没必要,所以应该综合考虑之下采用了 List 为基础,暴力循环去重。

三、HashMap 的线程不安全

关于 HashMap 的相关问题,源码里已经分析过,大体是这样的。

不安全:

  • 普通读写不一致问题;
  • 死循环问题;
  • ConcurrentModificationException 异常。

解决:

  • util包的Hashtable集合线程安全;
  • 用 synchronizedMap(new HashMap())包装;
  • 使用 juc 包的 ConcurrentHashMap。

HashMap 和 ConcurrentHashMap 的源码分析:

HashMap源码解析、jdk7和8之后的区别、相关问题分析

ConcurrentHashMap源码解析,多线程扩容

到此这篇关于java的各种集合为什么不安全(List、Set、Map)以及代替方案的文章就介绍到这了,更多相关java 集合不安全内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解java各种集合的线程安全

    线程安全 首先要明白线程的工作原理,jvm有一个main memory,而每个线程有自己的working memory,一个线程对一个variable进行操作时,都要在自己的working memory里面建立一个copy,操作完之后再写入main memory.多个线程同时操作同一个variable,就可能会出现不可预知的结果.根据上面的解释,很容易想出相应的scenario. 而用synchronized的关键是建立一个monitor,这个monitor可以是要修改的variable也可以其

  • Java 集合中的类关于线程安全

    Java集合中那些类是线程安全的 线程安全类 在集合框架中,有些类是线程安全的,这些都是jdk1.1中的出现的.在jdk1.2之后,就出现许许多多非线程安全的类. 下面是这些线程安全的同步的类: vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用.在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的. statck:堆栈类,先进后出 hashtable:就比hashmap多了个线程安全 enumeration:枚举,相当于迭代器

  • 浅谈java中对集合对象list的几种循环访问

    java中对集合对象list的几种循环访问的总结如下  1 经典的for循环 public static void main(String[] args) { List<String> list = new ArrayList(); list.add("123"); list.add("java"); list.add("j2ee"); System.out.println("=========经典的for循环=======

  • 基于Java回顾之集合的总结概述

    Java中的集合主要集中在2部分,一部分是java.util包中,一部分是java.util.concurrent中,后者是在前者的基础上,定义了一些实现了同步功能的集合. 这篇文章主要关注java.util下的各种集合对象.Java中的集合对象可以粗略的分为3类:List.Set和Map.对应的UML图如下(包括了java.util下大部分的集合对象):Collection概述 Java集合中的List和Set都从Collection出来,它是一个学习集合很不错的入口,它包含了集合中通常需要有

  • 深入剖析java中的集合框架

    解析:如果并不知道程序运行时会需要多少对象,或者需要更复杂方式存储对象,那么可以使用Java集合框架. 如果启用集合的删除方法,那么集合中所有元素的索引会自动维护. 集合完全弥补了数组的缺陷. 02.集合框架的内容 集合框架都包含三大块内容:对外的接口,接口的实现和对集合运算的算法 01.接口:表示集合的抽象数据类型 02.实现:集合框架中接口的具体实现 03.算法:在一个实现了某个集合框架的接口的对象身上完成某种有用的计算方法 java集合框架简图: 01.Collection接口存在储存一组

  • Java中List集合的遍历实例详解

     一.对List的遍历有三种方式   List<String> list = new ArrayList<String>(); list.add("testone"); list.add("testtwo"); ... 第一种: for(Iterator<String> it = list.iterator(); it.hasNext(); ) { .... } 这种方式在循环执行过程中会进行数据锁定,    性能稍差,    同

  • 如何去除Java中List集合中的重复数据

    1.循环list中的所有元素然后删除重复 public class duplicatRemoval { public static List removeDuplicate(List list){ for(int i=0;i<list. size()-1;i++){ for(int j=list.size()-1;j>i;j--){ if(list. get(j). equals(list.get(i))){ list.remove(j); } } } return list; } } 总结:

  • 解决JAVA遍历List集合,删除数据时出现的问题

    一.问题描述 有时候,我们会遇到在遍历List集合的过程中删除数据的情况. 看着自己写的代码,感觉完全没有问题,但就是达不到预期的效果,这是为什么呢?下面我们来分析下 String str1 = new String("1"); String str2 = new String("2"); String str3 = new String("3"); String str4 = new String("4"); String

  • 一分钟了解Java中List集合与set集合的多种遍历方式

    List集合与set集合的多种遍历方式 方法有: 1. for循环遍历 2. foreach增强循环遍历 3. 迭代器遍历 4. lambda表达式遍历 一.List集合的遍历 1.创建一个集合,添加一些元素放在集合当中 public static void main(String[] args) { //int[] ins = new int[8];//int List<String> list = new ArrayList<String>();//List,ArrayList

  • java用list集合存储学生信息并算出成绩平均值操作

    需求 键盘输入五名学生信息并录入list集合; 输出每个学生的信息,计算并输出这五个学生Java语言成绩的平均值: 计算并输出他们Java语言成绩的最大值和最小值. 思路 用Scanner 键盘输入 用for循环依次录入学生信息 用for循环输出学生信息 用for循环拿出学生成绩并求出成绩平均分 代码 补充知识:java 计算平均值,去除不合理的数据 我就废话不多说了,大家还是直接看代码吧~ package com.ine.tool; import java.util.ArrayList; im

  • Java中List集合对象去重及按属性去重的8种方法

    最近在写一些关于java基础的文章,但是我又不想按照教科书的方式去写知识点的文章,因为意义不大.基础知识太多了,如何将这些知识归纳总结,总结出优缺点或者是使用场景才是对知识的升华.所以我更想把java相关的基础知识进行穿针引线,进行整体上的总结. 总结java中创建并写文件的5种方式 总结java从文件中读取数据的6种方法 总结java创建文件夹的4种方法及其优缺点 总结java中删除文件或文件夹的7种方法 总结java中文件拷贝剪切的5种方式 比如之前我已经写了上面的这些内容,如果对java基

  • 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(U

随机推荐