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


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

  1. 加锁
  2. 获取到当前的集合数组;
  3. 计算长度;
  4. 调用 Arrays.copyOf 方法进行添加操作 , 每次只添加一个元素进去;
  5. 修改引用 , 更新最新的集合;
  6. return true 。
  7. 解锁
其中的 lock 在源码里就是一个:
java的各种集合为什么不安全(List、Set、Map)?文章插图
可以看到是一个普通的 Object 。
那么加锁的时候就用 synchronized 对 Object 进行加锁 , 没有采用 juc 的 ReetrantLock , 注释li也写了 , 偏向于使用内置的 monitor 也就是 synchronized 底层 monitor 锁 , 这一点也充分说明了 synchronized 的性能更新使得源码作者使用它 。
这个方法是处理最直接的 , 其他对应的写操作:remove、set等等也是一样的基础流程 。
我们再来看看读操作 get 方法:
java的各种集合为什么不安全(List、Set、Map)?文章插图
2|0二、HashSet 的不安全2|12.1 问题及原因我们还是用 List 一样的测试代码;
public class TestSet {public static void main(String[] args) {HashSet 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();}}}就会看到一样的错误:
java的各种集合为什么不安全(List、Set、Map)?文章插图
2|22.2 出现问题的原因其实从出现 ConcurrentModificationException 异常来看 , 我们可以猜测是和 List 类似的原因导致的异常 。
可以看到 , 源码里面 , Set 的底层维护的是一个 HashMap 来实现 。 对于遍历操作来说 , 都是一样的使用了 fail-fast iterator 迭代器 , 因此会出现这个异常 。
另外 , 因为 HashSet 的底层是 HashMap, 本质上 , 对于每一个 key, 保证唯一 , 使用了一个 value 为 PRESENT 常量的键值对进行存储 。
java的各种集合为什么不安全(List、Set、Map)?文章插图
put 的过程也是调用 map 的 put 方法 。
2|32.3 解决方案
  • List 有对应的 Vector 可用 , 本来就是线程安全的集合 , 但是 Set 没有;
  • 数据量小的时候 , 使用 Collections.synchronizedSet(new HashSet<>()) 这种方式 , 来包裹这个集合 , 上面我们使用 List 的时候也有类似的方法;
  • 同样的 , juc包为我们提供了新的线程安全集合 CopyOnWriteArraySet() 。
2|42.4 CopyOnWriteArraySet
  1. 按照前面的思路 , List 的对应线程安全集合是在 List 集合的数组基础上进行加锁的相关操作 。
  2. 那么 Set 既然底层是 HashMap , 对应的线程安全集合就应该是对 HashMap 的线程安全集合进行加锁 , 或者说直接用 ConcurrentHashMap 集合来实现 CopyOnWriteArraySet。
  3. 但事实上 , 源码并不是这么做的 。
从名字来看 , 和 ConcurrentHashMap 也没有什么关系 , 而是类似 CopyOnWriteArrayList 的命名 , 说明是读写单独处理 , 来让他成为线程安全的集合 , 那为什么是 ArraySet 多一个 array 修饰语呢?
java的各种集合为什么不安全(List、Set、Map)?文章插图
可以看到 , 他的思路没有顺延 util 包的 HashSet 的实现思路 , 而是直接使用了 CopyOnWriteArrayList 作为底层数据结构 。 也就是说没有利用 Map 的键值对映射的特性来保证 set 的唯一性 , 而是用一个数组为基底的列表来实现 。 (那显然在去重方面就要做额外的操作了 。 )
然后每一个实现的方法都很简单 , 基本是直接调用了 CopyOnWriteArrayList 的方法:
java的各种集合为什么不安全(List、Set、Map)?文章插图
我们最担心的可能 产生问题的 remove 和 add 方法 , 也是使用了 CopyOnWriteArrayList 的方法:
而保证 set 的不重复性质的关键 , 显然就在于 CopyOnWriteArrayList 的 addIfAbsent 方法 , 我们还是点进 CopyOnWriteArrayList 源码看一看这个方法的实现:
java的各种集合为什么不安全(List、Set、Map)?文章插图
其中的 indexOfRange 方法:
java的各种集合为什么不安全(List、Set、Map)?文章插图
可以看到 , 也是加了 Monitor 锁来进行的 , 整个过程是这样的:
  1. 获取本来的 set, 是一个数组 , 以快照形式返回当前的数组;
  2. indexOfRange 方法通过遍历查找查找元素出现位置 , addIfAbsent方法完成不存在则加入 , 如果前一个为 false 后一个就不会执行;