写的方法,我们首先可以看 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 的方法: