HashTable, HashMap, ConcurrentHashMap
这三个的主要区别在线程安全方面,HashMap 本身不是线程安全的.HashTable,ConcurrentHashMap是线程安全的,因为HashMap众所周知,所以我们重点来讨论剩下的两个
HashTable 线程安全实现机制
核心原理:全局对象锁
HashTable 通过最基础的互斥锁实现线程安全:
public synchronized V put(K key, V value) { ... }
public synchronized V get(Object key) { ... }
public synchronized V remove(Object key) { ... }
// 所有公共方法均使用synchronized修饰
就是很粗暴的直接针对 Hashtable 对象本⾝加锁.这肯定存在一些问题
- 如果多线程访问同⼀个 Hashtable 就会直接造成锁冲突.
- size 属性也是通过 synchronized 来控制同步, 也是⽐较慢的.
- ⼀旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到⼤量的元素拷⻉, 效率会⾮常低.
ConcurrentHashMap的线程安全实现机制
ConcurrentHashMap是通过精细的锁控制和无锁技术结合来实现高并发的线程安全。最核心的思路是把锁的粒度做得尽可能细,让不同的线程操作不同的数据区域时不会相互阻塞。它用synchronized锁单个哈希桶而不是整个表,这样不同桶的读写操作可以同时进行。对于空桶的初始化操作,它直接用无锁的CAS操作解决,避免了不必要的锁开销。
在具体实现上,每次操作时先根据key定位到哈希桶。如果桶是空的,就尝试用CAS创建新节点;如果桶不为空,就锁住桶的第一个节点再进行操作。链表和红黑树结构采用不同的处理方式,特别在转树过程中有专门的锁机制确保安全。这样保证了即使多个线程同时操作同一个桶,只要第一个节点锁住,后续操作就有序进行。
读操作处理得更轻量级,完全不加锁。通过所有节点字段都使用volatile修饰,包括节点值val和后继节点next,确保了线程修改后其他线程能立即看到变化。计数方式采用LongAdder风格的分片计数,把size拆成多个部分进行累加,减少了线程间的计数冲突。
遇到扩容时设计也很精妙。一个线程触发的扩容操作,其他写入线程发现后也会一起参与数据迁移,分片区协作完成。这种协同式扩容避免了全局阻塞问题。通过这些设计组合,在保证线程安全的前提下实现了接近HashMap的单线程性能。
总结
HashMap: 线程不安全. key 允许为 null
Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象, 效率较低. key 不允许为 null.
ConcurrentHashMap: 线程安全. 使用 synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用CAS 机制. 优化了扩容方式. key 不允许为 null