HashTable, HashMap, ConcurrentHashMap都是Map接口的实现类,都是以key-value的形式来存储数据

HashMap

  1. HashMap 的键值可以为null (当key为空时,哈希会被赋值为0)
  2. HashMap 的默认初始容量是16, 最大容量是2^30
  3. HashMap 使用的数据结构是 数组 + 链表 + 红黑树
    如果链表中结点个数 > 8时,链表 将转化为 红黑树
    如果链表中结点个数 < 6时,红黑树 又转化为 链表
    如果哈希桶中某条链表的个数 > 8时,并且桶的个数超过64时,才会将链表转换为红黑树。否则就直接扩容
  4. HashMap 效率非常高,但线程不安全

HashTable

  1. HashTable 的键值不能为null
  2. HashTable 虽然线程安全,但只是简单得用 synchronized 给所有方法加锁,相当于是对this加锁,也就是对整个HashTable对象进行加锁(非常无脑)
    一个HashTable对象只有一把锁,如果两个线程访问同一个对象时,就会发生锁冲突
  3. HashTable效率非常低,因为无脑加锁原因,比如一些读操作不存在线程不安全问题,所以这样的加锁方式导致效率非常低
    比如 某个线程触发了扩容机制,那就会由这个线程完成整个扩容过程,如果元素特别多的情况下,效率非常低,其他线程阻塞等待的时间会特别长
  4. HashTable 使用的数据结构是 数组 + 链表

ConcurrentHashMap

  1. ConcurrentHashMap 的键值不可以为null
  2. ConcurrentHashMap 使用的数据结构是 数组 + 链表 + 红黑树
  3. ConcurrentHashMap 最重要的点要说 线程安全,ConcurrentHashMap 相比比较于HashTable 有很多的优化,最核心的思路就是:降低锁冲突的概率
  • 锁粒度的控制
    ConcurrentHashMap 不是锁整个对象,而是使用多把锁,对每个哈希桶(链表)都进行加锁,只有当两个线程同时访问同一个哈希桶时,才会产生锁冲突,这样也就降低了锁冲突的概率,性能也就提高了
    
  • ConcurrentHashMap 只给写操作加锁,读操作没加锁
    如果两个线程同时修改,才会有锁冲突
    如果两个线程同时读,就不会有锁冲突
    如果一个线程读,一个线程写,也是不会有锁冲突的
    (这个操作也是可能会锁冲突的,因为有可能,读的结果是一个修改了一半的数据
    不过ConcurrentHashMap在设计时,就考虑到这一点,就能够保证读出来的一定时一个“完整的数据”,要么是旧版本数据,要么是新版本数据,不会是读到改了一半的数据;而且读操作中也使用到了volatile保证读到的数据是最新的)
    
  • 充分利用到了CAS的特性
    比如更新元素个数,都是通过CAS来实现的,而不是加锁
    
  • ConcurrentHashMap 对于扩容操作,进行了特殊优化
    HashTable的扩容是这样:当put元素的时候,发现当前的负载因子已经超过阀值了,就触发扩容。
    扩容操作时这样:申请一个更大的数组,然后把这之前旧的数据给搬运到新的数组上
    但这样的操作会存在这样的问题:如果元素个数特别多,那么搬运的操作就会开销很大
    执行一个put操作,正常一个put会瞬间完成O(1)
    但是触发扩容的这一下put,可能就会卡很久(正常情况下服务器都没问题,但也有极小概率会发生请求超时(put卡了,导致请求超时),虽然是极小概率,但是在大量数据下,就不是小问题了)
    ConcurrentHashMap 在扩容时,就不再是直接一次性完成搬运了
    而是搬运一点,具体是这样的
    扩容过程中,旧的和新的会同时存在一段时间,每次进行哈希表的操作,都会把旧的内存上的元素搬运一部分到新的空间上,直到最终搬运完成,就释放旧的空间
    在这个过程中如果要查询元素,旧的和新的一起查询;如果要插入元素,直接在新的上插入;如果是要删除元素,那就直接删就可以了
    
03-29 04:29