从0到1重新认识Java(面试篇)之HashMap(下)

184 阅读3分钟

这是我参与8月更文挑战的第31天,活动详情查看:8月更文挑战

前言

关于HashMap,其实问的比较多的还是HashMap和ConcurrentHashMap的区别,这样一连串的问下去,一环接一环;先问问区别,然后按着线程安全问下去,这样一直以来,考验了基础又看当前面试者的能力

历练

上一篇文章,主要讲述的是HashMap以及 关于JDK1.7的ConcurrentHashMap结构与安全性,本篇文章将着重讲解JDK1.8的ConcurrentHashMap

什么是CAS ? 自旋又是什么?

在此之前,需要先了解一下,什么是CAS,以及CAS的实现---自旋

CAS 是乐观锁的一种实现方式,是一种轻量级锁,JUC中很多工具类的实现都是基于CAS;

CAS的操作流程是:

  1. 线程在读取数据时不进行加锁
  2. 在准备执行修改数据时,比较原值是否已经被修改,若未被修改则写入成功;若已经被修改则报错或者重新走读取数据
  3. 这是一种客观的策略,认为并发操作不总是发生

CAS就一定能保证数据没被其他线程修改吗

并不是的,比如最经典的ABA问题,CAS就没办法判断了;
一个线程修改了值为B,另一个线程又把值修改为A,对于这个时候判断的线程,就只能发现值还是A,所以不知道这个值到底有没有被修改过。其实如果只追求最后结果正确,这是OK的。

但是实际过程中还是需要记录修改的过程的,比如资金修改、日志监控这些业务,都是需要记录过程的,方便追溯。

如何解决ABA问题?

用版本号去保证,在数据库加一个字段,VersionNumber,每次判断的时候校验版本号,操作成功则+1(where num = #{num})

除此以外,时间戳也可以做到,查询的时候把时间戳也查出来,对比一致才修改,并且更新当前时间;

方法很多。跟版本号异曲同工,看业务场景的设计

既然用锁或 synchronized 关键字可以实现原子操作,那么为什么还要用 CAS 呢

因为加锁或使用 synchronized 关键字带来的性能损耗较大,而用 CAS 可以实现乐观锁,它实际上是直接利用了 CPU 层面的指令,所以性能很高。 synchronized 之前一直都是重量级的锁,但是后来Java官方是有对他进行升级的,现在采用的是锁升级的方式去做的

针对synchronized 获取锁的方式,JVM使用了锁升级的优化方式,就是使用偏向锁优先同一线程然后再次获取锁,如果失败,则升级为CAS轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。如果最后都失败了就升级为重量级锁。

很多重量级都是由轻量级的方式锁定的。

关于JDK1.8 的ConcurrentHashMap

  • Put()方法

    • 根据Key 计算出 hashcode,如果没有初始化就调用 initTable()
    • 利用Key 的hashcode 定位出Node,利用CAS 写入
    • 如果 位置 hashcode == MOVED == -1 ,则需要扩容
    • 如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
    • 最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环
    • 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容
  • Get()方法

    • 计算hash值,定位到该table的索引位置,如果是首节点符合就返回

    • 如果是红黑树就遍历;链表也遍历

总结

关于面试常见的集合三连,这里只是小打小闹,真正的了解+掌握,还得多实践+多去巩固