Skip to content

面试速答(先看这里)

**一句话结论:**ConcurrentHashMap在JDK 1.8之前使用分段锁保证线程安全, ConcurrentHashMap默认情况下将hash表分为16个桶(分片),在加锁的时候,针对每个单独的分片进行加锁,其他分片不受影响。

60秒标准回答:

HashMap是非线程安全的

Hashtable 中的方法是同步的,所以它是线程安全的

ConcurrentHashMap在JDK 1.8之前使用分段锁保证线程安全, ConcurrentHashMap默认情况下将hash表分为16个桶(分片),在加锁的时候,针对每个单独的分片进行加锁,其他分片不受影响。锁的粒度更细,所以他的性能更好

**答题顺序:**结论 → 原理/机制 → 关键流程 → 场景与取舍 → 易错点

回答主线:

  • **要点1:**ConcurrentHashMap在JDK 1.8中,采用了一种新的方式来实现线程安全,即使用了CAS+synchronized,这个实现被称为"分段锁"的变种,也被称为"锁分离",它实现的锁的粒度更细。
  • **要点2:**HashMap继承了抽象类AbstractMap实现了Map接口。
  • **要点3:**ConcurrentHashMap同样继承了抽象类AbstractMap,并且实现了ConcurrentMap接口。
  • **要点4:**ConcurrentHashMap,默认初始容量为16,默认的加载因子为0.75,即当ConcurrentHashMap中元素个数超过容量的75%时,会进行扩容操作。
  • **要点5:**ConcurrentHashMap使用分段锁机制,因此在遍历时需要注意,遍历时ConcurrentHashMap的某个段被修改不会影响其他段的遍历。

**记忆锚点:**ConcurrentHashMap → HashMap → Hashtable → key → AbstractMap → null

关键取舍:

  • 锁的粒度更细,所以他的性能更好。

易错提醒:

  • 扩容时,容量会扩大为原来的两倍,并会采用分段锁机制,将ConcurrentHashMap分为多个段(segment),每个段独立进行扩容操作,避免了整个ConcurrentHashMap的锁竞争。
  • 这个过程也会判断是否存在并发修改: ConcurrentHashMap使用分段锁机制,因此在遍历时需要注意,遍历时ConcurrentHashMap的某个段被修改不会影响其他段的遍历。

加分表达:

  • 如果只需要遍历HashMap中的key或value,可以使用KeySet或Values来遍历。

追问准备:

  • 围绕「ConcurrentHashMap」:底层原理是什么?使用时有哪些边界和常见坑?
  • 围绕「HashMap」:底层原理是什么?使用时有哪些边界和常见坑?
  • 围绕「Hashtable」:底层原理是什么?使用时有哪些边界和常见坑?
  • 如果线上出现异常,你会如何定位、验证并规避?

典型回答 ​

线程安全:

HashMap是非线程安全的。

Hashtable 中的方法是同步的,所以它是线程安全的。

ConcurrentHashMap在JDK 1.8之前使用分段锁保证线程安全, ConcurrentHashMap默认情况下将hash表分为16个桶(分片),在加锁的时候,针对每个单独的分片进行加锁,其他分片不受影响。锁的粒度更细,所以他的性能更好。

ConcurrentHashMap在JDK 1.8中,采用了一种新的方式来实现线程安全,即使用了CAS+synchronized,这个实现被称为"分段锁"的变种,也被称为"锁分离",它实现的锁的粒度更细。

📄 ✅ConcurrentHashMap是如何保证线程安全的?

打开文档:✅ConcurrentHashMap是如何保证线程安全的?

继承关系:

Hashtable是基于陈旧的Dictionary类继承来的。

HashMap继承了抽象类AbstractMap实现了Map接口。

ConcurrentHashMap同样继承了抽象类AbstractMap,并且实现了ConcurrentMap接口。

允不允许null值:

HashTable中,key和value都不允许出现null值,否则会抛出NullPointerException异常。

HashMap中,null可以作为键或者值都可以。

ConcurrentHashMap中,key和value都不允许为null。

📄 ✅为什么ConcurrentHashMap不允许null值?

打开文档:✅为什么ConcurrentHashMap不允许null值?

默认初始容量和扩容机制:

HashMap的默认初始容量为16,默认的加载因子为0.75,即当HashMap中元素个数超过容量的75%时,会进行扩容操作。扩容时,容量会扩大为原来的两倍,并将原来的元素重新分配到新的桶中。

Hashtable,默认初始容量为11,默认的加载因子为0.75,即当Hashtable中元素个数超过容量的75%时,会进行扩容操作。扩容时,容量会扩大为原来的两倍加1,并将原来的元素重新分配到新的桶中。

ConcurrentHashMap,默认初始容量为16,默认的加载因子为0.75,即当ConcurrentHashMap中元素个数超过容量的75%时,会进行扩容操作。扩容时,容量会扩大为原来的两倍,并会采用分段锁机制,将ConcurrentHashMap分为多个段(segment),每个段独立进行扩容操作,避免了整个ConcurrentHashMap的锁竞争。

遍历方式的内部实现上不同 :

HashMap使用EntrySet进行遍历,即先获取到HashMap中所有的键值对(Entry),然后遍历Entry集合。支持fail-fast,也就是说在遍历过程中,若HashMap的结构被修改(添加或删除元素),则会抛出ConcurrentModificationException。如果只需要遍历HashMap中的key或value,可以使用KeySet或Values来遍历。

Hashtable使用Enumeration进行遍历,即获取Hashtable中所有的key,然后遍历key集合。这个过程也会判断是否存在并发修改:

java
public T next() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    return nextElement();
}

public void remove() {
    if (!iterator)
        throw new UnsupportedOperationException();
    if (lastReturned == null)
        throw new IllegalStateException("Hashtable Enumerator");
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();

    synchronized(Hashtable.this) {
        Entry[] tab = Hashtable.this.table;
        int index = (lastReturned.hash & 0x7FFFFFFF) % tab.length;

        for (Entry<K,V> e = tab[index], prev = null; e != null;
             prev = e, e = e.next) {
            if (e == lastReturned) {
                modCount++;
                expectedModCount++;
                if (prev == null)
                    tab[index] = e.next;
                else
                    prev.next = e.next;
                count--;
                lastReturned = null;
                return;
            }
        }
        throw new ConcurrentModificationException();
    }
}

ConcurrentHashMap使用分段锁机制,因此在遍历时需要注意,遍历时ConcurrentHashMap的某个段被修改不会影响其他段的遍历。可以使用EntrySet、KeySet或Values来遍历ConcurrentHashMap,其中EntrySet遍历时效率最高。遍历过程中,ConcurrentHashMap的结构发生变化时,不会抛出ConcurrentModificationException异常,但是在遍历时可能会出现数据不一致的情况,因为遍历器仅提供了弱一致性保障。

特性/集合类HashMapHashtableConcurrentHashMap
线程安全否是,基于方法锁是,基于分段锁(分段锁是个概念,并不是说只有Segment那种才叫分段锁,详见CHM的不同版本介绍的文章。)
继承关系AbstractMapDictionaryAbstractMap,ConcurrentMap
允许null值K-V都允许K-V都不允许K-V都不允许
默认初始容量161116
默认加载因子0.750.750.75
扩容后容量原来的两倍原来的两倍加1原来的两倍
是否支持fail-fast支持支持fail-safe