HashMap 与 Hashtable 区别

面试官问:"HashMap 和 Hashtable 有什么区别?"

候选人小尤答:"Hashtable 是线程安全的,HashMap 不是。"

面试官点点头:"那为什么生产中很少用 Hashtable?"

小尤说:"因为性能差?"

面试官追问:"具体哪些操作性能差?为什么?"

小尤答不上来。

【面试官心理】 这道题考查的是候选人对并发编程和集合框架演进的理解。能说出"全局 synchronized"和"不能存 null key"的候选人,说明真正理解过为什么 Hashtable 被淘汰。

一、核心区别对比表 🔴

维度HashMapHashtable
线程安全❌ 不安全✅ 安全(所有方法 synchronized)
性能快(无锁)慢(全局锁)
null 支持key 可以为 null,value 可以为 nullkey/value 都不能为 null
迭代器快速失败(fail-fast)快速失败(fail-fast)
初始容量1611
扩容策略2 倍2 倍 + 1
继承关系继承 AbstractMap继承 Dictionary
迭代器类型IteratorEnumeration(遗留)+ Iterator
适用场景单线程(高并发用 ConcurrentHashMap)不推荐(用 ConcurrentHashMap)

二、Hashtable 的线程安全问题 🔴

2.1 全部方法加 synchronized

public class Hashtable<K, V> {
    public synchronized V put(K key, V value) { }
    public synchronized V get(Object key) { }
    public synchronized V remove(Object key) { }
    public synchronized int size() { }
    // 所有 public 方法都是 synchronized
}

问题:每次访问都需要获取整个 Hashtable 对象的锁,高并发下性能极差。

2.2 为什么被淘汰

// Hashtable 的 put 和 get:
public synchronized V put(K key, V value) {
    // ...
}

public synchronized V get(Object key) {
    // ...
}

// 多线程并发场景:
// 线程1: hashtable.put(key1, value1)  // 获取锁
// 线程2: hashtable.get(key2)          // 等待锁!
// 线程3: hashtable.size()            // 等待锁!
// 所有线程排队执行,并发度为 1

2.3 ConcurrentHashMap 的解决方案

// JDK 8 ConcurrentHashMap:
// 不锁整个表,只锁单个桶
// 写操作:synchronized (node) { ... }
// 读操作:CAS + volatile(无锁)
// 并发度大幅提升

三、null 限制的差异 🔴

3.1 Hashtable 不允许 null

Hashtable<String, String> table = new Hashtable<>();
table.put(null, "value");  // ❌ NullPointerException
table.put("key", null);    // ❌ NullPointerException
table.get(null);           // ❌ NullPointerException

3.2 HashMap 允许 null

HashMap<String, String> map = new HashMap<>();
map.put(null, "value");    // ✅ 允许
map.put("key", null);      // ✅ 允许
map.get(null);             // ✅ 允许

// 但要注意:null key 只能有一个
map.put(null, "v1");
map.put(null, "v2"); // 覆盖
map.size(); // 1

3.3 为什么 Hashtable 不允许 null

// Hashtable 的 put 方法:
public synchronized V put(K key, V value) {
    if (value == null) {
        throw new NullPointerException();
    }
    // 无法区分"key 不存在返回 null"和"value 为 null"
    // 因为 get 可能返回 null(value 为 null)或 null(key 不存在)
}

// 如果允许 null:
map.get("notExistKey"); // 返回 null
map.get("nullValueKey"); // 也返回 null
// 无法区分两种情况!

四、Collections.synchronizedMap 的陷阱 🟡

// Collections 包装后的 Map
Map<String, Object> map = Collections.synchronizedMap(new HashMap<>());

// 所有方法都加了 synchronized
// 内部实现:
public V put(K key, V value) {
    synchronized (mutex) { return m.put(key, value); }
}

4.1 原子操作问题

// ❌ 错误:不是原子操作
Map<String, Integer> map = Collections.synchronizedMap(new HashMap<>());
map.put("key", 0);
map.put("key", map.get("key") + 1); // 不是原子!

// 两个线程可能同时读取到 0,各自加 1,存入 1
// 期望:2,实际:1

// ✅ 正确:使用 ConcurrentHashMap
ConcurrentHashMap<String, LongAdder> map = new ConcurrentHashMap<>();
map.computeIfAbsent("key", k -> new LongAdder()).increment();

五、选型建议 🔴

// 单线程场景
Map<String, Object> map = new HashMap<>(); // ✅ 推荐

// 高并发场景
Map<String, Object> map = new ConcurrentHashMap<>(); // ✅ 推荐

// 缓存场景
Map<String, Object> map = new ConcurrentHashMap<>();
// 或使用 CacheBuilder (Guava)

// 需要有序遍历
Map<String, Object> map = new TreeMap<>(); // 单线程
Map<String, Object> map = new ConcurrentSkipListMap<>(); // 多线程

【面试官心理】 能说出 Collections.synchronizedMap 的原子操作陷阱的候选人,说明有并发编程的实战经验。这是 P6 的要求。