1. 多线程环境使用哈希表
HashMap 本身不是线程安全的.
在多线程环境下使用哈希表可以使用:
- Hashtable(不推荐使用)
- ConcurrentHashMap(推荐使用)
1)HashMap
- 首先HashMap本身线程不安全
- 其次HashMap的key值可以为空
下面是HashMap的hash方法,可以看出如果key值为空,hash方法的返回值为0
- HashMap 扩容是等某一次 put 后发现负载因子不达标后开始扩容, 新建一个表, 重新将元素哈希(具体细节有机会再讲)
2)Hashtable
- Hashtable源码中的put方法中的key如果为空,hashCode方法会抛出空指针异常
比如:
- **HashTable的线程安全只是简单的把关键方法加上了 synchronized 关键字. **
相当于直接针对 Hashtable 对象本身加锁.
-
如果多线程访问同一个 Hashtable 就会直接造成锁冲突.
-
size 属性也是通过 synchronized 来控制同步, 也是比较慢的.
-
一旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低.
一个Hashtable只有一把锁,两个线程访问Hashtable中的任意数据都会出现锁竞争,就如上图,有两个线程要操作这两个元素,由于是一把大锁,就会产生竞争,但是仔细分析,这两操作在不同的哈希桶上,不牵扯修改同一个变量,因此就不会发生线程安全,所以上面的锁竞争是没有必要的。
如果两个修改落到同一个哈希桶上,有线程安全风险
3)ConcurrentHashMap
相比之下ConcurrentHashMap就做出了重大的改进,把锁的粒度细化了
-
读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然 是是用 synchronized, 但是不是锁整个对象, 而是 “锁桶” (用每个链表的头结点作为锁对象), 大大降 低了锁冲突的概率.
-
充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.
-
优化了扩容方式: 化整为零
- 发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.
- 扩容期间, 新老数组同时存在.
- 后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小 部分元素.
- 搬完最后一个元素再把老数组删掉.
- 这个期间, 插入只往新数组加.
- 这个期间, 查找需要同时查新数组和老数组
此时一个哈希表上面的桶的个数可能会非常多,导致出现锁冲突的概率,大大降低了
扩容=申请更大的内存+搬运元素(搬运元素过程肯定要重新哈希)
上面讲的都是基于Java 8里的;在Java1.7里,ConcurrentHashMap不是每个桶一个锁,而是“分段锁”一个锁管若干个桶
- ConcurrenHashMap的key也不可以为空,看下面的源码中的put方法,如果key==null则会抛出空指针异常
总结HashTable, HashMap, ConcurrentHashMap 之间的区别
HashMap: 线程不安全. key 允许为 null
Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象, 就是一把大锁,锁冲突极高,效率较低. key 不允许为 null.
ConcurrentHashMap: 线程安全. 使用 synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用 CAS 机制. 优化了扩容方式. key 不允许为 null