问题:

  1. JDK1.7和JDK1.8的ConcurrentHashMap底层数据结构有何区别?
  2. JDK1.7和JDK1.8的ConcurrentHashMap实现原理有何区别?
  3. HashMap、ConcurrentHashMap主要有何区别?
  4. ConcurrentHashMap初始大小、加载因子分别是多少?
  5. ConcurrentHashMap的put方法的主要流程?
  6. 在JDK1.8中ConcurrentHashMap,为何要用synchronized代替ReentrantLock?
  7. ConcurrentHashMap和HashTable有何区别?
  8. 为什么ConcurrentHashMap比HashTable更高效?
  9. ConcurrentHashMap的get方法的主要流程?
  10. ConcurrentHashMap读写需要加锁么?为何读不用加锁?
  11. ConcurrentHashMap迭代器是强一致性还是弱一致性?HashMap呢?

1. JDK1.7和JDK1.8的ConcurrentHashMap底层数据结构有何区别?

  • 1.7 版本ConcurrentHashMap 是由一个 Segment 数组和多个 HashEntry 组成
  • Segment 数组的意义就是将一个大的 table 分割成多个小的 table 来进行加锁(分段锁技术),而每一个 Segment 元素存储的是 HashEntry 数组。
  • JDK1.8 的实现废弃了 Segment,使用 Node数组+链表+红黑树的数据结构来实现。在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容版本。

JDK1.7:

在这里插入图片描述

JDK1.8: (规则和HashMap差不多)

在这里插入图片描述

2. JDK1.7和JDK1.8的ConcurrentHashMap实现原理有何区别?

  • 数据结构不同

  • 线程安全机制不同:

    JDK1.7是 ReentrantLock, JDK1.8是 CAS + synchronized

  • 锁的粒度

    JDK1.7是对需要进行数据操作的 Segment 加锁,1.8 是对每个数组元素加锁(Node)

3. HashMap、ConcurrentHashMap主要有何区别?

  • ConcurrentHashMap 是线程安全的,HashMap 不是线程安全的
  • 除了加锁,原理上没有太大区别。另外,HashMap 的键值对允许有null,但是 ConcurrentHashMap 都不允许。

4. ConcurrentHashMap初始大小、加载因子分别是多少?

ConcurrentHashMap 和 HashMap 的初始大小都是 16,加载因子都是 0.75.

ThreadLocalMap 初始大小为 16,加载因子为 2/3.

加载因子:是衡量哈希表密集程度的一个参数,加载因子与hash冲突(过高不良)的可能性成正相关,与装载量成正相关,与内存使用率(过低不良)成正相关。该值取值应该考虑到内存使用率和hash冲突概率的平衡

5. ConcurrentHashMap的put方法的主要流程?

  1. 如果没有初始化就先调用initTable方法来进行初始化过程

  2. 如果没有hash冲突就直接 CAS 插入

  3. 如果还在进行扩容操作就先进行扩容

  4. 如果有 hash 冲突,则加锁来保证线程安全。有两种情况;1、链表形式就直接遍历到尾端插入(尾插法),2、红黑树就按照红黑树结构插入

    先插入,然后再判断是否转为红黑树。

  5. 如果链表数量大于阈值 8,且数组长度大于 64,则转换为红黑树

    先插入,再(可能)转为红黑树,然后再判断是否扩容。

  6. 如果添加成功就调用 addCount方法 统计 size,并检查是否需要扩容(在 transfer方法 中真正扩容,会有一个newtable)。

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {//循环是为了CAS+自旋
            Node<K,V> f; int n, i, fh;
            //如果没有初始化就先调用initTable方法来进行初始化过程
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //如果没有hash冲突就直接 CAS 插入
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //如果还在进行扩容操作就先进行扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            //如果不在扩容操作,就锁定Node并做添加操作
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        //链表形式就直接遍历到尾端插入(尾插法)
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        //红黑树就按照红黑树结构插入
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                //如果链表数量大于阈值 8,且数组长度大于 64,则转换为红黑树
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
    	//如果添加成功就调用 addCount方法统计size,并检查是否需要扩容
        addCount(1L, binCount);
        return null;
    }

6. 在JDK1.8中ConcurrentHashMap,为何要用synchronized代替ReentrantLock?

  • 减少内存开销

    使用 ReentrantLock 则需要节点继承 AQS 来获得同步支持,增加内存开销,而1.8中只有头结点需要进行同步,(后续的节点是可以不用锁的,也就不需要全都继承 AQS 来获取同步支持)。

  • 内部优化

    synchronized 则是 JVM 直接支持的,JVM 能够在运行时做出相应的优化措施:锁粗化、锁消除、锁自旋等等。(见锁优化)

7. ConcurrentHashMap和HashTable有何区别?

相同点:

  • Hashtable 和 ConcurrentHashMap 都是线程安全的,可以在多线程环境中运行, key 和 value 都不能是 null;

不同点:

  • Hashtable 使用一把锁(锁住整个链表结构,synchronized 修饰了 put 方法)处理并发问题,多线程竞争一把锁,容易阻塞。
  • JDK1.8中,ConcurrentHashMap 使用 CAS+非公平自旋 + synchronized(Node) + Node + 红黑树。锁粒度: Node(首节点),锁粒度降低了。
  • ConcurrentHashMap 的性能要比 Hashtable 要好。

8. 为什么ConcurrentHashMap比HashTable更高效?

ConcurrentHashMap 使用 CAS+非公平自旋 + synchronized(Node) + volatile + Node + 红黑树。锁粒度: Node(首节点),锁粒度降低了。

Hashtable 使用一把锁(锁住整个链表结构,synchronized 修饰了 put 方法)处理并发问题,多线程竞争一把锁,容易阻塞。

9. ConcurrentHashMap的get方法的主要流程?

  1. 计算 hash 值,定位到该 table 索引位置,如果是首节点符合就返回
  2. 如果遇到扩容的时候,会调用标志正在扩容节点 ForwardingNode 的 find 方法,查找该节点,匹配就返回
  3. 以上都不符合的话(又不是首节点,也不再扩容,说明要么在链表中,要么在红黑树中),就往下遍历节点,匹配就返回,否则最后就返回 null

get 的时候不需要加锁,因为 Node 变量是被 volatile 修饰的,有可见性。只要写操作保证了线程安全,读线程就能安全。

 public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {
                //计算 hash 值,定位到该 table 索引位置,如果是首节点符合就返回
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                //如果遇到扩容的时候,会调用标志正在扩容节点 ForwardingNode 的 find 方法,查找该节点,匹配就返回
                return (p = e.find(h, key)) != null ? p.val : null;
            //以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回 null
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

10. ConcurrentHashMap读写需要加锁么?为何读不用加锁?

写加锁,读不用加锁.

Node 元素 val 和指针 next 是用 volatile 修饰的,在多线程环境下线程A修改因为 hash 冲突修改节点的 val 或者新增节点,对其他线程都是及时可见的(见 volatile 如何保证原子性:volatile 与 内存屏障

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;

         ... 
}

11. ConcurrentHashMap迭代器是强一致性还是弱一致性?HashMap呢?

什么是强一致性与弱一致性

ConcurrentHashMap 迭代器是弱一致性,HashMap 强一致性。

ConcurrentHashMap 可以支持在迭代过程中,向 map 添加新的元素,而 HashMap 则会抛出 ConcurrentModificationException,因为 HashMap 包含一个修改计数器,当调用 HashMap 的 next() 方法获取下一个元素的时候,迭代器将会用到这个计数器。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐