Java HashMap和ConcurrentHashMap知识点梳理

  1. jdk 8 HashMap 扩容之后旧元素存放位置是?
    java 在扩容的时候会创建一个新的 Node<K,V>[],用于存放扩容之后的值,并将旧的Node数组(其大小记作n)置空;至于旧值移动到新的节点的时候存放于哪个节点是根据 (e.hash & oldCap) == 0 来判断的:
    ① 等于0时,则将其索引位置h不变;
    ② 不等于0时,则将该节点在新数组的索引位置等于原索引位置加上旧数组长度。

  2. Java 中的另一个线程安全的、与 HashMap 极其类似的类是什么?同样是线程安全,它与 Hashtable 在线程同步上有什么不同?
    ConcurrentHashMap 类(是 Java并发包 java.util.concurrent 中提供的一个线程安全且高效的 HashMap 实现)。Hashtable 是使用 synchronized 关键字加锁的原理(就是对对象加锁);而针对 ConcurrentHashMap,在 JDK 7 中采用分段锁的方式,而JDK 8 中直接采用了CAS(无锁算法)+ synchronized。

  3. HashMap & ConcurrentHashMap 的区别?
    除了加锁,原理上无太大区别。另外,HashMap 的键值对允许有null,但是ConCurrentHashMap 都不允许。

  4. 为什么 ConcurrentHashMap 比 Hashtable 效率要高?
    Hashtable 使用一把锁(锁住整个链表结构)处理并发问题,多个线程竞争一把锁容易导致阻塞;而ConcurrentHashMap降低了锁粒度。ConcurrentHashMap在JDK 7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于 Segment,包含多个 HashEntry。在JDK 8 中,使用 CAS + synchronized + Node + 红黑树,锁粒度为Node(首结点)(实现 Map.Entry)。

  5. ConcurrentHashMap 在 JDK 8 中,为什么要使用内置锁 synchronized 来代替重入锁 ReentrantLock?
    ①、粒度降低了;
    ②、JVM 开发团队没有放弃 synchronized,而且基于 JVM 的 synchronized 优化空间更大,更加自然。
    ③、在大量的数据操作下,对于 JVM 的内存压力,基于 API 的 ReentrantLock 会开销更多的内存。

  6. ConcurrentHashMap 的并发度是什么?
    程序运行时能够同时更新 ConccurentHashMap 且不产生锁竞争的最大线程数。默认为 16,且可以在构造函数中设置。
    当用户设置并发度时,ConcurrentHashMap 会使用大于等于该值的最小2幂指数作为实际并发度(假如用户设置并发度为17,实际并发度则为32)

  7. ConcurrentHashMap 加锁机制
    它加锁的场景分为两种:
    1、没有发生hash冲突的时候,添加元素的位置在数组中是空的,使用CAS的方式来加入元素,这里加锁的粒度是数组中的元素。
    2、如果出现了hash冲突,添加的元素的位置在数组中已经有了值,那么又存在三种情况。
    (1)key相同,则用新的元素覆盖旧的元素。
    (2)如果数组中的元素是链表的形式,那么将新的元素挂载在链表尾部。
    (3)如果数组中的元素是红黑树的形式,那么将新的元素加入到红黑树。
    第二种场景使用的是synchronized加锁,锁住的对象就是链表头节点(红黑树的根节点)
    ,加锁的粒度和第一种情况相同。
    结论:ConcurrentHashMap分段加锁机制其实锁住的就是数组中的元素,当操作数组中不同的元素时,是不会产生竞争的。

  8. ConcurrentHashMap存储对象时(put() 方法):
    1> 如果没有初始化,就调用 initTable() 方法来进行初始化;
    2> 如果没有哈希冲突就直接 CAS 无锁插入;
    3> 如果需要扩容,就先进行扩容;
    4> 如果存在哈希冲突,就加锁来保证线程安全,两种情况:一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入;
    5> 如果该链表长度大于阀值 8(且数组中元素数量大于64),就要先转换成红黑树的结构。

  9. 下⾯这个⽅法保证了 HashMap 总是使⽤ 2 的幂作为哈希表的⼤⼩。
    /**
    * Returns a power of two size for the given target capacity.
    */
    static final int tableSizeFor(int cap) {
    int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

  10. HashMap红黑树退化为链表的阈值为什么是6?
    为了避免频繁地进行链表和红黑树的转化。

  11. ConcurrentHashMap get操作需要加锁吗?线程安全吗?
    get 方法不需要加锁。因为 Node 的元素 value 和指针 next 是用 volatile 修饰的,在多线程环境下线程A修改节点的 value 或者新增节点的时候是对线程B可见的。这也是它比其它并发集合比如 Hashtable、用 Collections.synchronizedMap()包装的 HashMap 效率高的原因之一。

posted @ 2021-04-30 17:52  楼兰胡杨  阅读(409)  评论(0编辑  收藏  举报