
Hashtable 的 synchronized 是方法级锁,锁整个对象
你调用 put()、get()、size() 甚至 containsKey(),全都会锁住整个 Hashtable 实例。这意味着哪怕两个线程操作的是完全不重叠的 key(比如一个改 "user:1001",一个读 "order:9999"),也得排队等锁释放。
常见错误现象:高并发下吞吐量骤降,jstack 看到一堆线程 BLOCKED 在 Hashtable.get 或 put 上;迭代 entrySet() 时特别慢,因为整个表被锁死,连读都不能并发。
-
Hashtable不允许null作为 key 或 value,否则运行时报NullPointerException - 继承自古老的
Dictionary,API 设计过时(比如还留着elements()和keys()这种Enumeration方法) - 默认初始容量是 11,扩容为
old * 2 + 1,和HashMap的 2 的幂次扩容不兼容
Collections.synchronizedMap 是代码块级锁,但仍是全表锁
Collections.synchronizedMap(new HashMap()) 包装出来的对象,内部用一个 mutex 对象做同步锚点,所有修改/查询方法都用 synchronized(mutex) 包裹——看起来比 Hashtable “精细”一点,实际锁粒度没变,还是整张 map 互斥。
关键差异在于:你可以传入自定义的锁对象,比如 Collections.synchronizedMap(map, myLock),从而和其他业务逻辑共用同一把锁,避免死锁或简化同步控制。
立即学习“Java免费学习笔记(深入)”;
- 允许
nullkey 和 value(底层是HashMap,行为一致) - 不是“真正线程安全”的银弹:像
if (!map.containsKey(k)) map.put(k, v)这种复合操作,仍需外层加synchronized,否则竞态照旧 - 迭代时必须手动同步:要遍历,得写
synchronized (map) { for (Entry e : map.entrySet()) ... },漏了就可能抛ConcurrentModificationException
ConcurrentHashMap 才是真正的分段/桶级锁,性能差距巨大
这才是现代 Java 里该用的线程安全 Map。ConcurrentHashMap 在 JDK 8+ 用 CAS + synchronized 锁单个桶(table[i]),读操作基本无锁,写操作只锁冲突的那条链或红黑树节点。16 个桶默认就能支持最多 16 个线程同时写不同 key —— 而不是 Hashtable 那样“一人写,全员等”。
性能影响直观:5 线程各塞 50 万数据,ConcurrentHashMap 通常比 Hashtable 快 5–10 倍,比 synchronizedMap 快 3–6 倍(取决于 CPU 核数和 key 分布)。
- 不保证实时一致性:
get()可能看不到其他线程刚put()的值(弱一致性),但对绝大多数业务场景完全够用 - 没有
size()的精确原子值,size()是估算的(要精确得用mappingCount(),返回long) -
computeIfAbsent()、merge()这类原子复合操作,是ConcurrentHashMap独有的能力,synchronizedMap和Hashtable都不支持
别再用 Hashtable,synchronizedMap 仅适合简单过渡
Hashtable 是 JDK 1.0 的遗留物,官方文档明确标注为“legacy”,连 Javadoc 都建议用 ConcurrentHashMap 替代。它没泛型、没 lambda 支持、没现代 Map 接口方法(如 replaceAll),连 forEach 都得自己包装。
synchronizedMap 唯一合理场景是:你已有现成的 HashMap 实例,又不想大改接口,临时加一层同步兜底。但只要涉及并发写、迭代、条件更新,它就会暴露“有条件线程安全”的本质——表面安全,实则脆弱。
真正该做的,是从设计阶段就选 ConcurrentHashMap:初始化时明确是否需要排序(不用)、是否允许 null(它也不允许)、是否要强一致性(一般不需要)。这些取舍一旦想清楚,后面就几乎不用操心锁的事了。










