1、两者的主要区别
- 线程安全:HashMap 是线程不安全的,当出现多线程操作时,会出现安全隐患;而 ConcurrentHashMap 是线程安全的。
- 并发操作:HashMap 不支持并发操作,没有同步方法,ConcurrentHashMap 支持并发操作,通过继承 ReentrantLock(JDK 1.7 重入锁)/CAS 和 synchronized (JDK 1.8 内置锁)来进行加锁(分段锁),每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
- 引入版本:HashMap 是 JDK 1.2 引入的,ConcurrentHashMap 是 JDK 1.5 引入的
- 允许空值:HashMap 的键和值允许使用空值,ConcurrentHashMap 如果使用空值会报空指针异常
2、原理
2.1 HashMap 在多线程下的问题
死循环问题:
假设存在 A 和 B 两个线程同时对 HashMap 扩容,两个线程执行速度有快慢,当 A 线程对 HashMap 完成扩容后,B 线程才刚刚将一个元素移动到新表,在 JDK1.7 版本中,由于采用头插法,那么最后 B 线程插入这个值的 next 会指向前面的值,造成死循环
2.2 ConcurrentHashMap 的线程安全
Get 方法:
- 为输入的 Key 做 Hash 运算,得到 hash 值。
- 通过 hash 值,定位到对应的 Segment 对象。
- 再次通过 hash 值,定位到 Segment 当中数组的具体位置。
Put 方法: - 为输入的 Key 做 Hash 运算,得到 hash 值。
- 通过 hash 值,定位到对应的 Segment 对象。
- 获取可重入锁。
- 再次通过 hash 值,定位到 Segment 当中数组的具体位置。
- 插入或覆盖 HashEntry 对象。
- 释放锁。
从步骤可以看出,ConcurrentHashMap 在读写时需要二次定位,首先定位到 Segment,之后定位到 Segment 内的具体数组下标。
Size 方法: - 遍历所有的 Segment。
- 把 Segment 的元素数量累加起来。
- 把 Segment 的修改次数累加起来。
- 判断所有 Segment 的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。
- 如果尝试次数超过阈值,则对每一个 Segment 加锁,再重新统计。
- 再次判断所有 Segment 的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。
- 释放锁,统计结束。