Map、List的安全问题

91 阅读5分钟

HashMap

Put 原理

  1. 将k,v封装到 Node 对象当中;
  2. 调用 K 的 hashCode() 方法得出 hash 值;
  3. 通过哈希算法,将 hash 值转换成数组的下标,下标位置上如果没有任何元素,就把 Node 添加到这个位置上。如果位置上有链表就会拿着 k 和链表上每个节点的 k 进行 equal。如果所有的 equals 方法返回都是 false,那么这个新的节点将被添加到链表的末尾。如其中有一个 equals 返回了true,那么这个节点的 value 将会被覆盖。

Get 原理

  1. 先调用 k 的 hashCode() 方法得出哈希值,并通过哈希算法转换成数组的下标;
  2. 通过数组下标快速定位到某个位置上。如果这个位置上什么都没有,则返回 null。如果这个位置上有单向链表,那么它就会拿着参数 K 和单向链表上的每一个节点的 K 进行 equals,如果所有 equals 方法都返回 false,则 get 方法返回 null。如果其中一个节点的 K 和参数 K 进行 equals 返回 true,那么 get 方法最终返回这个要找的 value。

扩容机制

当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值---即当前数组的长度乘以加载因子的值的时候,就要自动扩容。 HashMap 的容量,默认是 16,加载因子,默认是 0.75,假设加载因子为 0.75,HashMap 的初始容量为 16,当 HashMap 中有 16 * 0.75 = 12 个容量时,HashMap 就会进行扩容。如果加载因子越大,扩容发生的频率就会比较低,占用空间比较小,但是发生 hash 冲突的几率会提升,对元素操作时间会增加,运行效率降低;如果加载因子太小,那么表中的数据将过于稀疏(很多空间还没用,就开始扩容了),对空间造成严重浪费;

安全问题

  1. 数据不一致:一个线程在插入键值对时,另一个线程可能正在删除或修改相同的键值对,从而导致数据不一致;
  2. 死锁:一个线程在等待另一个线程释放锁时,可能会被阻塞,从而导致死锁;
  3. 竞态条件:一个线程在读取 HashMap 时,另一个线程可能正在修改 HashMap,从而导致竞态条件;
  4. 在 JDK1.7 中,当并发执行扩容操作时会造成环形链和数据丢失的情况;
  5. 在 JDK1.8 中,在并发执行 put 操作时会发生数据覆盖的情况。

ConcurrentHashMap

ConcurrentHashMap 优势就是采用了分段锁,数据被分成多个段(segment),每一个 Segment 就好比一个自治区,读写操作高度自治,Segment 之间相互不受影响,Segment本身就相当于一个HashMap对象。

Put 原理

  1. 为输入的 Key 做 Hash 运算,得到hash值
  2. 通 过hash 值,定位到对应的 Segment 对象
  3. 获取可重入锁
  4. 再次通过 hash 值,定位到 Segment 当中数组的具体位置
  5. 插入或覆盖 HashEntry 对象
  6. 释放锁

Get 原理

  1. 为输入的 Key 做 Hash 运算,得到 hash 值
  2. 通过 hash 值,定位到对应的 Segment 对象
  3. 再次通过 hash 值,定位到 Segment 当中数组的具体位置

并发读写的情形

  1. 不同 Segment 的并发写入,是可以并发执行的;
  2. 同一 Segment 的一写一读,可以并发执行的;
  3. 同一 Segment 的并发写入,需要上锁的,因此对同一Segment的并发写入会被阻塞。

如何保证 Size 数据准确

  1. 遍历所有的 Segment,把 Segment 的元素数量累加起来。
  2. 把 Segment 的修改次数累加起来。
  3. 判断所有 Segment 的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。
  4. 如果尝试次数超过阈值,则对每一个 Segment 加锁,再重新统计。
  5. 再次判断所有 Segment 的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。
  6. 释放锁,统计结束。

这种思想和乐观锁悲观锁的思想如出一辙,为了尽量不锁住所有 Segment,首先乐观地假设 Size 过程中不会有修改。当尝试一定次数,才无奈转为悲观锁,锁住所有 Segment 保证强一致性

List 和 CopyOnWriteArrayList

在多线程情况下,List 的安全问题和 HashMap 安全问题的1~3一样,可以使用 CopyOnWriteArrayList 解决安全问题,CopyOnWriteArrayList 通过使用写时复制技术来保证线程安全。 当一个线程试图修改 CopyOnWriteArrayList 时,它会创建一个新的副本,然后在副本上进行修改。当修改完成后,它会将副本替换原始列表。这样,其他线程在读取列表时,仍然可以访问原始列表,而不会受到修改线程的影响。因此,CopyOnWriteArrayList 可以在多线程环境下安全地进行读写操作。 需要注意的是,由于写时复制技术的使用,CopyOnWriteArrayList 在写入操作时会产生一定的性能开销。因此,在不需要线程安全的情况下,建议使用 ArrayList 而不是 CopyOnWriteArrayList。