目录

 

前言

一、谈谈你理解的 HashMap,讲讲其中的 get put 过程。

二、1.8 做了什么优化?

三、HashMap是线程安全的嘛?

四、ConcurrentHashMap 是如何实现的?


前言

Map 这样的键值对key-value结构在软件开发中是非常经典的结构,常用于在内存中存放数据。

看了些技术文章,总结了一下HashMap和ConcurrentHashMap的核心知识点,采用提问解答的方式,相信看完后以后你会感觉清晰很多~

 

一、谈谈你理解的 HashMap,讲讲其中的 get put 过程。

答: HashMap 底层是基于数组+链表组成的容器,key-value允许空值,不同步、线程不安全

Put过程

  1. 先判断数组是否需要初始化;
  2. 如果key为空,则put一个null值进去;
  3. 根据key计算出hashcode;
  4. 根据hashcode定位出所在的捅;
  5. 如果捅是一个链表,则要判断里面的hashcode、key是否和传入的key相等,如果相等则进行覆盖,并返回原来的值;
  6. 如果捅是空的,说明当前位置没有数据存入,新增一个Entry对象写入当前位置;
  7. 当调用 addEntry 写入 Entry 时需要判断是否需要扩容。

Get过程

  1. 根据key计算hashcode,定位到具体的桶;
  2. 判断该位置是否是链表,不是链表就根据key、key的hashcode是否相等来返回值,是链表就遍历直到key以及hashcode相等的时候就返回值;
  3. 啥都没取到就返回null.

二、1.8 做了什么优化?

  1. 优化了查询效率,TREEIFY_THRESHOLD 用于判断是否需要将链表转换为红黑树的阈值。
  2. 抛弃了segment分段锁,采用CAS+Synchronized来保证并发安全性
  3. 数据结构上发生很大改动,HashEntry 修改为 Node

三、HashMap是线程安全的嘛?

不是,jdk没有对HashMap做任何同步操作

  • 不安全会导致哪些问题?

并发环境下容易产生死循环,发生死锁

  • 如何解决?有没有线程安全的并发容器?

可以使用Collections.synchronizedMap(Map map)方法,可以把hashmap变成一个同步的容器 (拥有锁限制的同步机制,内部加了synchronized锁同步关键字)

有,ConcurrentHashMap,内部采用了ReentrantLock可重入锁,有效避免死锁

四、ConcurrentHashMap 是如何实现的?

由segment数组,HashEntry组成,仍是数组+链表,与HashMap类似,只是内部的Value以及链表都是volatile关键字修饰,保证其可见性。

ConcurrentHashMap使用了分段锁技术,segment继承于RenntrantLock可重入锁,默认16个,线程相互独立。(1.7)

  • 1.7、1.8 实现有何不同?为什么这么做?

1.7:

HashMap和ConcurrentHashMap的灵魂拷问

Put操作:(由于volatile只能保证数据的可见性和顺序性,无法保证原子性,所以仍需加锁)

首先通过key定位到segment,然后再到具体的segment进行put,

将当前segment中的table根据key的hashcode定位到HashEntry,遍历该HashEntry,不为空判断传入的key和当前遍历的key是否相等,相等则覆盖旧的值。为空则新建一个HashEntry加入当前segment中,并判断是否需要扩容。最后解除获取的segment锁。

Get操作: 

只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。

由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。

ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁

1.8:(解决那就是查询遍历链表效率太低。)

HashMap和ConcurrentHashMap的灵魂拷问

1.8 抛弃了segment分段锁,取消了 ReentrantLock 改为了synchronized,采用CAS+synchronized保证并发安全性。

将Entry改为Node,val和next用volatile修饰。

采用红黑树之后可以保证查询效率

CAS:CompareAndSwag比较交换,实现多线程同步的原子指令

Put操作:

  • 首先根据key计算hashcode;
  • 判断是否需要初始化;
  • 通过key定位Node,判断是否为空。空:可直接写数据,CAS尝试写入,若失败,则自旋写入;
  • 若当前Node位置 hashcode==MOVED==-1 ,则可扩容;
  • 若都不满足则synchronized锁简单粗暴写;
  • 若数量大于treeify_thresod树阈值。则转为红黑树

Get操作:

  • 根据key hashcode 寻址。
  • 若在桶上,直接返回,若是红黑树,就按照树的方式遍历。
  • 若都不满足就用链表的方式遍历。

 

 

 

 

 

 

 

 

 

相关文章: