HashMap 不是线程安全的
HashTable 和 ConcurrentHashMap 是线程安全的
在多线程环境下更推荐使用 ConcurrentHashMap ,效率更高
加锁粒度
锁粒度不同,HashTable 触发锁竞争的频率更高。
- HashTable 只是简单的在关键方法上加了
synchronized
,相当于直接对 HashTable 对象加锁,锁的粒度粗,这导致锁竞争会比较严重。
哈希表 是数组+链表的结构,在进行插入操作时,通过 key 计算 hash 值,就可以通过下标找到对应的链表进行插入。
两个线程同时进行插入操作:
t1 线程插入的元素, 对应下标为 1 的链表。
t2 线程插入的元素, 对应下标为 2 的链表。
两个线程修改不同变量,是没有线程安全问题的,
但是由于对 HashTable 对象加锁的,这两个线程还是会产生锁竞争。
- ConcurrentHashMap 就不一样了,不像 HashTable 一样简单的对整个对象加锁,而是针对每个链表加锁,以链表的头结点作为加锁对象,这样只有在修改同一个链表的时候才会触发锁竞争。
CAS 机制
ConcurrentHashMap 充分利用了 CAS 机制,例如更新 size 使用了 CAS 操作,进一步降低锁竞争频率。
扩容方式
HashTable 扩容方式:
- 一个线程操作 HashTable 触发扩容后,这个线程要将旧数组上的全部元素拷贝到新数组上,效率较低,长时间持有锁,可能导致锁升级为重量级锁,进一步降低效率。
ConcurrentHashMap 扩容方式:
-
当一个线程触发扩容后,会创建一个新的数组,然后搬一小部分元素到新数组上。
-
扩容期间新老数组同时存在,后续操作 ConcurrentHashMap 的线程都会参与扩容。
-
每次操作都会搬一小部分元素到新数组上,直到搬空旧的数组。
-
搬空旧数组后,删除旧数组,扩容过程结束。
-
扩容期间,插入新元素都插到新数组上,查找操作需要查新数组和老数组
总结
- HashMap:线程不安全,key 允许为 null。
- Hashtable: 线程安全,使用 synchronized 锁 Hashtable 对象,效率较低,key 不允许为 null。
- ConcurrentHashMap:线程安全,使用 synchronized 锁每个链表头结点,锁冲突概率低,,充分利用CAS 机制,优化了扩容方式.,key 不允许为 null。