ConcurrentHashMap(并发性)面试知识点总结:

1,145 阅读5分钟

1.HashTable怎么实现线程安全?以及为什么效率低下

  • HashTable在进行put和get的时候会对整个hash表上锁(synchronized),虽然能保证线程安全,但是效率低下。

2. Hashtable 是不允许键或值为 null ?

  • Hashtable使用的是安全失败机制(fail-safe),这种机制会使你此次读到的数据不一定是最新的数据

    • 什么是fail-safe: fail-safe:这种遍历基于容器的一个克隆。因此,对容器内容的修改不影响遍历。java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。

    • 优点:采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。 原文链接:blog.csdn.net/striner/art…

    • 缺点: 迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的

  • 如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次【contain(key):是用于单线程的hashMap用来判断是否存在还是空】下来对key是否存在进行判断,ConcurrentHashMap同理

    • hashtable为什么就不能containKey(key) :一个线程先get(key)再containKey(key),这两个方法的中间时刻,其他线程怎么操作这个key都会可能发生,例如删掉这个key

3.什么是fail-fast:

  • 是java集合中的一种机制, 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception

  • 原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出ConcurrentModificationException异常,终止遍历。作者:why技术链接:juejin.cn/post/684490…

  • 注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。作者:why技术链接:juejin.cn/post/684490…

  • 场景: java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改) ,这是一种安全机制。

4.ConcurrentHashMap1.7结构:

  • Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表

    • HashEntry是用volatile是修饰的:

    • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性

    • 禁止进行指令重排序。(实现有序性

    • volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性

  • 优点:它在进行put和get的时候就会先进行segment定位segment数量就是数组的大小,初始为16.可以允许16个线程同时操作。

  • 缺点: 数组加链表的方式,查询的时候,还得遍历链表,会导致效率很低

5.ConcurrentHashMap1.8结构:

  • CAS + synchronized 来保证并发安全性。

  • 同样在节点中把值和next采用了volatile去修饰,保证了可见性,并且也引入了红黑树,在链表大于一定值的时候会转换(默认是8)

  • put:

  1. 根据 key 计算出 hashcode 。

  2. 判断是否需要进行初始化。

  3. 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功

  4. 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容

  5. 如果都不满足,则利用 synchronized 锁写入数据。

  6. 如果数量> TREEIFY_THRESHOLD 则要转换为红黑树

  • 什么是CAS:

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

    • CAS 操作的流程大体是,

      1. 线程在读取数据时不进行加锁

      2. 在准备写回数据时,比较原值是否修改

      3. 若未被其他线程修改则写回,若已被修改,则重新执行读取流程

    • 这是一种乐观策略,认为并发操作并不总会发生

    • CAS和ABA问题:

      • 什么是ABA:

        1. 原来是数据是A,第一个线程把值改回了B,

        2. 又来了一个线程把值又改回了A

        3. 对于这个时候判断的线程,就发现他的值还是A,所以他就不知道这个值到底有没有被人改过,其实很多场景如果只追求最后结果正确,这是没关系的。

        4. 特殊场景(资金交易):但是实际过程中还是需要记录修改过程的,比如资金修改什么的,你每次修改的都应该有记录,方便回溯

        5. 解决方法: 修改前去查询他原来的值的时候再带一个版本号,每次判断就连值和版本号一起判断,判断成功就给版本号+1;其实加时间戳也是行的,主要就是为了区别过程。

  • 为什么使用Synchronized(重量级):

    • 首先:synchronized之前一直都是重量级的锁,但是后来java官方是对他进行过升级的,他现在采用的是锁升级的方式去做的。

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

    • 所以是一步步升级上去的,最初也是通过很多轻量级的方式锁定的

  • 参考文章: mp.weixin.qq.com/s/AixdbEiXf…