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

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

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

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

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

  • Hashtable使用的是安全失败机制(fail-safe),这种机制会使你此次读到的数据不一定是最新的数据
  • 什么是fail-safe: fail-safe:这种遍历基于容器的一个克隆。因此,对容器内容的修改不影响遍历。java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。
  • 优点:采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception
    原文链接:https://blog.csdn.net/striner/article/details/86375684
  • 缺点: 迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的
  • 如果你使用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技术链接:https://juejin.im/post/5e1bf3b7f265da3e4b5be39a
  • **注意:**这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。作者:why技术链接:https://juejin.im/post/5e1bf3b7f265da3e4b5be39a
  • 场景: 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 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁
    • 所以是一步步升级上去的,最初也是通过很多轻量级的方式锁定的
  • 参考文章: https://mp.weixin.qq.com/s/AixdbEiXf3KfE724kg2YIw

你可能感兴趣的:(ConcurrentHashMap(并发性)面试知识点总结:)