您当前的位置: 首页 >  面试
  • 0浏览

    0关注

    674博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

面试:ConcurrentHashMap相关

沙漠一只雕得儿得儿 发布时间:2021-12-20 19:08:02 ,浏览量:0

Q1:为什么ConcurrentHashMap的读操作不需要加锁?

get没有加锁的话,ConcurrentHashMap是如何保证读到的数据不是脏数据的呢?

答:value和next指针使用了volatile来保证其可见性。对于get操作,其实没有线程安全的问题,只有可见性的问题,只需要确保get的数据是线程之间可见的即可。

用volatile修饰的Node:

get操作可以无锁是由于Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。

volatile登场

对于可见性,Java提供了volatile关键字来保证可见性、有序性。但不保证原子性。 普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

  • volatile关键字对于基本类型的修改可以在随后对多个线程的读保持一致,但是对于引用类型如数组,实体bean,仅仅保证引用的可见性,但并不保证引用内容的可见性。。
  • 禁止进行指令重排序。

总结:

  • 在1.8中ConcurrentHashMap的get操作全程不需要加锁,这也是它比其他并发集合比如hashtable、用Collections.synchronizedMap()包装的hashmap;安全效率高的原因之一。
  • get操作全程不需要加锁是因为Node的成员val是用volatile修饰的和数组用volatile修饰没有关系。
  • 数组用volatile修饰主要是保证在数组扩容的时候保证可见性。
Q2:put()方法是怎么加锁的

通过ConcurrentHashMap的put方法,我们可以发现,加锁分为了两种:

1、没有发生hash冲突的时候,如果添加的元素的位置在数组中是空的话,那么就使用CAS的方式来加入元素,这里加锁的粒度是数组中的元素

2、如果出现了hash冲突,添加的元素的位置在数组中已经有了值,那么又存在三种情况 (1)key相同,那么直接用新的元素覆盖旧的元素 (2)如果数组中的元素是链表的形式,那么将新的元素挂载在链表尾部 (3)如果数组中的元素是红黑树的形式,那么将新的元素加入到红黑树

第二种情况使用的是synchronized加锁,锁住的对象就是数组中的元素,加锁的粒度和第一种情况相同。得出结论,ConcurrentHashMap的分段加锁机制,其实锁住的就是数组中的元素,当操作数组中不同的元素时,是不会产生竞争的  

Q3:JDK1.7和JDK1.8的put()操作区别

1.7:put()操作:

1.将key传入put方法中,先根据key的hashcode的值找到对应的segment段

2.再根据segment中的put方法,加锁lock()。

3.再次hash确定存放的hashEntry数组中的位置

4.在链表中根据hash值和equals方法进行比较,如果相同就直接覆盖,如果不同就插入在链表中。

1.8作者:put()操作:

1.先判断Node数组有没有初始化,如果没有初始化先初始化initTable();

2.根据key的进行hash操作,找到Node数组中的位置,如果不存在hash冲突,即该位置是null,直接用CAS插入

3.如果存在hash冲突,就先对链表的头节点或者红黑树的头节点加synchronized锁

4.如果是链表,就遍历链表,如果key相同就执行覆盖操作,如果不同就将元素插入到链表的尾部,并且在链表长度大于8, Node数组的长度超过64时,会将链表的转化为红黑树。

5.如果是红黑树,就按照红黑树的结构进行插入。

Q4:ConcurrentHashMap和Hashtable的区别?

1.底层数据结构:

JDK1.7的ConcurrentHashMap底层采用:Segments数组+HashEntry数组+链表

JDK1.8的ConcurrentHashMap底层采用:Node数据+链表+红黑树

Hashtable底层数据结构采用:数组+链表

2.实现线程安全的方式:

在JDK1.7中ConcurrentHashMap采用分段锁实现线程安全。

在JDK1.8中ConcurrentHashMap采用synchronized和CAS来实现线程安全。

Hashtable采用synchronized来实现线程安全。在方法上加synchronized同步锁。

ConcurrentHashMap是如何实现线程安全的_|-| [- |_ |_ ()-CSDN博客_concurrenthashmap怎么保证线程安全

【大厂面试系列】并发容器之ConcurrentHashMap,吐血整理 - 知乎 

 Java后端面试高频问题:ConcurrentHashMap_技术交流_牛客网

ConcurrentHashMap分段加锁机制简析_LO_YUN的博客-CSDN博客_concurrenthashmap分段锁

1.ConcurrentHashMap底层实现

JDK1.7实现:

 底层数据结构:Segments数组+HashEntry数组+链表,采用分段锁保证安全性

一个ConcurrentHashMap中有一个Segments数组,一个Segments中存储一个HashEntry数组,每个HashEntry是一个链表结构的元素。

segment继承自ReentrantLock锁。

首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真正的并发访问。

get()操作:

HashEntry中的value属性和next指针是用volatile修饰的,保证了可见性,所以每次获取的都是最新值,get过程不需要加锁。

1.将key传入get方法中,先根据key的hashcode的值找到对应的segment段。

2.再根据segment中的get方法再次hash,找到HashEntry数组中的位置。

3.最后在链表中根据hash值和equals方法进行查找。

ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null。

put()操作:

1.将key传入put方法中,先根据key的hashcode的值找到对应的segment段

2.再根据segment中的put方法,加锁lock()。

3.再次hash确定存放的hashEntry数组中的位置

4.在链表中根据hash值和equals方法进行比较,如果相同就直接覆盖,如果不同就插入在链表中。

JDK1.8

底层数据结构:Node数组+链表+红黑树 采用Synchronized和CAS来保证线程安全。

get()操作:

get操作全程无锁。get操作可以无锁是由于Node元素的val和指针next是用volatile修饰的。

在多线程环境下线程A修改节点的val或者新增节点的时候是对线程B可见的。

1.计算hash值,定位到Node数组中的位置

2.如果该位置为null,则直接返回null

3.如果该位置不为null,再判断该节点是红黑树节点还是链表节点

如果是红黑树节点,使用红黑树的查找方式来进行查找

如果是链表节点,遍历链表进行查找

put()操作:

1.先判断Node数组有没有初始化,如果没有初始化先初始化initTable();

2.根据key的进行hash操作,找到Node数组中的位置,如果不存在hash冲突,即该位置是null,直接用CAS插入

3.如果存在hash冲突,就先对链表的头节点或者红黑树的头节点加synchronized锁

4.如果是链表,就遍历链表,如果key相同就执行覆盖操作,如果不同就将元素插入到链表的尾部,

并且在链表长度大于8, Node数组的长度超过64时,会将链表的转化为红黑树。

5.如果是红黑树,就按照红黑树的结构进行插入。

2.JDK1.8中为什么使用synchronized替换可重入锁ReentrantLock?

Segment继承了ReentrantLock,所以Segment是一种可重入锁。

1.Segment可重入锁锁住的是一个HashEntry数组,synchronized锁住的只是发生hash冲突的链表的头节点或红黑树的节点,提高了并发性能。

2.从JDK1.6开始,对 synchronized 锁的实现引入了大量的优化,并且 synchronized 有多种锁状态,会从偏向锁 -> 轻量级锁 -> 重量级锁一步步转换。

只要并发的线程可以在一定次数的自旋内拿到锁(偏向锁不用自旋),那么synchronized就不会升级为重量级锁,等待的线程也不会被挂起,减少了线程挂起和唤醒的切换的过程开销。

而ReentrantLock不会自旋,会直接挂起,这样一来就很容易会多出线程上下文开销的代价。

3.减少内存开销 。假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。

3.ConcurrentHashMap和Hashtable的区别?

1.底层数据结构:

JDK1.7的ConcurrentHashMap底层采用:Segments数组+HashEntry数组+链表

JDK1.8的ConcurrentHashMap底层采用:Node数据+链表+红黑树

Hashtable底层数据结构采用:数组+链表

2.实现线程安全的方式:

在JDK1.7中ConcurrentHashMap采用分段锁实现线程安全。

在JDK1.8中ConcurrentHashMap采用synchronized和CAS来实现线程安全。

Hashtable采用synchronized来实现线程安全。在方法上加synchronized同步锁。

4.HashMap与ConcurrentHashMap的区别?

HashMap是非线程安全的,这意味着不应该在多线程中对这些Map进行修改操作,否则会产生数据不 一致的问题,甚至还会因为并发插入元素而导致链表成环,这样在查找时就会发生死循环,影响到整个应用程序。

Collections工具类可以将一个Map转换成线程安全的实现,其实也就是通过一个包装类,然后把所有功能都委托给传入的Map,而包装类是基于synchronized关键字来保证线程安全的(Hashtable也是基于synchronized关键字),底层使用的是互斥锁,性能与吞吐量比较低。

ConcurrentHashMap的实现细节远没有这么简单,因此性能也要高上许多。

它没有使用一个全局锁来锁住自己,而是采用了减少锁粒度的方法,尽量减少因为竞争锁而导致的阻塞与冲突,而且ConcurrentHashMap的检索操作是不需要锁的。

5.ConcurrentHashMap是怎么分段分组的?

get操作:

Segment的get操作实现非常简单和高效,先经过一次再散列,然后使用这个散列值通过散列运算定位到 Segment,再通过散列算法定位到元素。get操作的高效之处在于整个get过程都不需要加锁,除非读到空的值才会加锁重读。原因就是将使用的共享变量定义成 volatile 类型。

put操作:

当执行put操作时,会经历两个步骤:

1. 判断是否需要扩容;

2. 定位到添加元素的位置,将其放入 HashEntry 数组中。

插入过程会进行第一次 key 的 hash 来定位 Segment 的位置,如果该 Segment 还没有初始化,即通过CAS 操作进行赋值,然后进行第二次 hash 操作,找到相应的 HashEntry 的位置,这里会利用继承过来的锁的特性,在将数据插入指定的 HashEntry 位置时(尾插***通过继承 ReentrantLock 的 tryLock() 方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该

Segment的锁,那当前线程会以自旋的方式去继续的调用 tryLock() 方法去获取锁,超过指定次数就挂起,等待唤醒。

关注
打赏
1657159701
查看更多评论
立即登录/注册

微信扫码登录

0.0403s