目录
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
- 但是在你的业务场景中,负载因子具体取多少,其最稳妥的办法 还是结合实际情况,选择不同的数值,进行性能测试,关注 时间 和 空间的开销,选择你认为最合适的值