2023.10.29 关于 HashTable 和 ConcurrentHashMap 区别

目录

HashTable

ConcurrentHashMap

优化点一

优化点二

优化点三

优化点四

不关键的小区别


HashTable

  • HashMap 和 HashTable 都是常见的哈希表数据结构,用于存储键值对

注意:

  • HashMap 是线程不安全的
  • HashTable 是线程安全的,其关键方法均加上了 synchronized,但也因此导致了性能上的开销

ConcurrentHashMap

  • 相比于 HashTable 我们更推荐使用  ConcurrentHashMap,相比于 HashTable ,其又进行了一定的线程安全优化

优化点一

  •  ConcurrentHashMap 相比于 HashTable 大大缩小了锁冲突的概率,把一把大锁转成多把小锁了
  •  HashTable 的做法是直接在方法上加 synchronized,等于是给 this 加锁,即只要操作 哈希表 上的任意元素,都会产生加锁,也就都可能发生锁冲突
  • 但是实际上,基于 哈希表 的结构特点,有些元素在进行并发操作的时候,是不会产生线程安全问题的,也就不需要使用锁控制

实例理解

  • 元素 1 、元素 2 在同一个链表上,元素 3 在另一个链表上

情况一

  • 如果此时 线程A 修改元素1,线程B 修改元素2,是否存在线程安全问题呢?
  • 可能存在
  • 比如这两个元素相邻,此时并发的插入或删除,就需要修改这俩节点相邻的节点的 next 的指向

情况二

  • 如果此时 线程A 修改元素1,线程B 修改元素3,是否存在线程安全问题?
  • 该情况相当于 多个线程 修改不同的变量,所以该情况不存在线程安全问题

  • 正是因为 HashTable ,其锁的冲突概率太大了,任何两个元素的操作都会有锁冲突,即使是在不同的链表上

  •  ConcurrentHashMap 做法是 让每个链表均有各自的锁,而不是所有链表共用同一个锁,也就是将锁的粒度变小
  • 具体来说就是使用每个链表的头结点,作为锁对象,两个线程针对同一个锁对象加锁,才有锁竞争,才有阻塞等待,针对不同的对象,没有竞争

注意:

  • 上述的 ConcurrentHashMap 是针对 JDK1.8 及以后的情况
  • 在 JDK1.7 和之前,ConcurrentHashMap 使用的是 分段锁

  • 分段锁,其本质上也是缩小锁的范围,从而降低锁冲突的概率
  • 但是这个做法并不彻底
  • 一是锁粒度分的还不够细
  • 二是代码实现也更加繁琐

优化点二

  • ConcurrentHashMap 针对读操作,不加锁,只针对写操作加锁
  • 读 和 读之间没有冲突
  • 写 和 写之间有冲突
  • 读 和 写之间也没有冲突

一般情况

  • 很多场景下,读写之间不加锁控制,可能会读到一个写了一半的结果
  • 如果写操作不是原子的,此时读就可能会读到写了一半的数据,相当于脏读

ConcurrentHashMap 优化做法

  • 使用 原子的写操作,来保证在 读写 场景下,线程读到的数据一定是完整的数据,而不是读到修改了一半的数据
  • 使用 volatile 关键字来保证及时从内存拿到修改后的数据

优化点三

  • ConcurrentHashMap 内部充分的使用了 CAS 操作
  • 以便通过 CAS 操作来进一步的削减加锁操作的数目
  • 比如维护元素个数、仅需 使用 CAS 操作进行 size++ 和 size--
  • 目的就是为了能尽可能降低锁冲突的概率,因为锁冲突对性能的影响很大

优化点四

  •  ConcurrentHashMap 针对扩容进行了优化,采取了 化整为零 的方式

HashTable 扩容方式:

  • 创建一个更大的数组空间,把旧的数组上的链表上的每个元素搬运到新的数组上(删除 + 插入)
  • 这个扩容操作会在某次 put 的时候进行触发
  • 如果元素个数特别多,就会导致这样的搬运操作,比较耗时
  • 就会出现,某次 put 比平时 put 卡很多倍(用户的感受:大部分用户用这好好的,某个用户就卡了

 ConcurrentHashMap 扩容方式:

  • ConcurrentHashMap 中,采取的是每次搬运一小部分元素的方式
  • 创建新的数组,旧的数组也保留
  • 每次 put 操作,都往新数组上添加,同时进行一部分搬运(把一小部分旧的元素搬运到新数组上)
  • 每个元素都是链表上的一个节点,其实就是 先删除旧数组上的节点 再插入到新数组的对应链表中 的操作
  • 每次 get 的时候,则 旧数组 和 新数组 一起查询
  • 每次 remove 的时候,直接删除该元素,无需搬运
  • 经过一定时间之后,所有元素都搬运好了,最终再释放旧数组 

不关键的补充

补充一

  • HashMap 的 key 允许为 null
  • ConcurrentHashMap 和 HashTable 的key 不允许为 null

补充二

  • 关于负载因子默认为 0.75
  • 但是在你的业务场景中,负载因子具体取多少,其最稳妥的办法 还是结合实际情况,选择不同的数值,进行性能测试,关注 时间 和 空间的开销,选择你认为最合适的值
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

茂大师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值