HashTable, HashMap, ConcurrentHashMap都是Map接口的实现类,都是以key-value的形式来存储数据
HashMap
- HashMap 的键值可以为null (当key为空时,哈希会被赋值为0)
- HashMap 的默认初始容量是16, 最大容量是2^30
- HashMap 使用的数据结构是
数组 + 链表 + 红黑树
如果链表中结点个数 > 8时,链表 将转化为 红黑树
如果链表中结点个数 < 6时,红黑树 又转化为 链表
如果哈希桶中某条链表的个数 > 8时,并且桶的个数超过64时,才会将链表转换为红黑树。否则就直接扩容 HashMap 效率非常高,但线程不安全
HashTable
- HashTable 的键值不能为null
- HashTable 虽然线程安全,但只是简单得用
synchronized
给所有方法加锁,相当于是对this加锁,也就是对整个HashTable对象进行加锁(非常无脑)
一个HashTable对象只有一把锁,如果两个线程访问同一个对象时,就会发生锁冲突 - HashTable效率非常低,因为无脑加锁原因,比如一些读操作不存在线程不安全问题,所以这样的加锁方式导致效率非常低
比如 某个线程触发了扩容机制,那就会由这个线程完成整个扩容过程,如果元素特别多的情况下,效率非常低,其他线程阻塞等待的时间会特别长 - HashTable 使用的数据结构是
数组 + 链表
ConcurrentHashMap
- ConcurrentHashMap 的键值不可以为null
- ConcurrentHashMap 使用的数据结构是
数组 + 链表 + 红黑树
- ConcurrentHashMap 最重要的点要说 线程安全,ConcurrentHashMap 相比比较于HashTable 有很多的优化,最核心的思路就是:
降低锁冲突的概率
- 锁粒度的控制
ConcurrentHashMap 不是锁整个对象,而是使用多把锁,对每个哈希桶(链表)都进行加锁,只有当两个线程同时访问同一个哈希桶时,才会产生锁冲突,这样也就降低了锁冲突的概率,性能也就提高了
- ConcurrentHashMap 只给写操作加锁,读操作没加锁
如果两个线程同时修改,才会有锁冲突 如果两个线程同时读,就不会有锁冲突 如果一个线程读,一个线程写,也是不会有锁冲突的 (这个操作也是可能会锁冲突的,因为有可能,读的结果是一个修改了一半的数据 不过ConcurrentHashMap在设计时,就考虑到这一点,就能够保证读出来的一定时一个“完整的数据”,要么是旧版本数据,要么是新版本数据,不会是读到改了一半的数据;而且读操作中也使用到了volatile保证读到的数据是最新的)
- 充分利用到了CAS的特性
比如更新元素个数,都是通过CAS来实现的,而不是加锁
- ConcurrentHashMap 对于扩容操作,进行了特殊优化
HashTable的扩容是这样:当put元素的时候,发现当前的负载因子已经超过阀值了,就触发扩容。 扩容操作时这样:申请一个更大的数组,然后把这之前旧的数据给搬运到新的数组上 但这样的操作会存在这样的问题:如果元素个数特别多,那么搬运的操作就会开销很大 执行一个put操作,正常一个put会瞬间完成O(1) 但是触发扩容的这一下put,可能就会卡很久(正常情况下服务器都没问题,但也有极小概率会发生请求超时(put卡了,导致请求超时),虽然是极小概率,但是在大量数据下,就不是小问题了) ConcurrentHashMap 在扩容时,就不再是直接一次性完成搬运了 而是搬运一点,具体是这样的 扩容过程中,旧的和新的会同时存在一段时间,每次进行哈希表的操作,都会把旧的内存上的元素搬运一部分到新的空间上,直到最终搬运完成,就释放旧的空间 在这个过程中如果要查询元素,旧的和新的一起查询;如果要插入元素,直接在新的上插入;如果是要删除元素,那就直接删就可以了