文章目录
- 0、考察范围
- 一、为什么有ConcurrentHashMap?
- 二、ConcurrentHashMap是什么,具体操作内容?
- 三、ConcurrentHashMap 怎么实现高效率地并发访问?实现机制
- 四、延申:其他并发使用
- 五、你都借鉴了哪些设计,并在实际中落地?
- 六、参考
0、考察范围
1、Java 的 HashMap 是如何实现的。按我的要求,候选人能够深入到内部算法实现,至少要把 HashMap 的内部数据结构实现和 get/put 算法给讲清楚。
2、能把装载因子(load factor)和再哈希(rehashing)給讲清楚。
3、延申:可以向 Java 集合框架方向延伸,也可以向 Java 多线程方向延伸,其中涉及的内容很多,例如 HashTable、ConcurrentHashMap,甚至 TreeMap、ConcurrentSkipList 和 ThreadLocal 等。HashMap 的基本原理并不难,但是延伸的内容如果没有实际动手实践过,你一般很难回答上来。
一、为什么有ConcurrentHashMap?
1、为什么会有ConcurrentHashMap?
HashMap线程不安全,HashTable效率低下。
追问1:为什么HashMap线程不安全?
1.7中transfer()链表使用头插法,多线程情况下,会成环;
1.8中putVal()若桶为空,多线程操作,值会出现覆盖情况。
追问2:为什么HashTable效率低下?
用synchronized锁住整个表来同步。线程竞争激烈时,一个线程访问HashTable同步方法,其他线程访问会处于阻塞或轮询状态。
二、ConcurrentHashMap是什么,具体操作内容?
1、1.8下ConcurrentHashMap 简单介绍?如何实现?(*6)
常见数据结构、put、get实现
1)sizeCtl 来控制了初始化、扩容大小,是否正在进行初始化和扩容
2)Node 继承至Entry,用于存储数据,是存储的基本单元,同时在基于Node的基础上,为了实现红黑树,扩展了TreeNode、TreeBin;TreeNode用于在红黑树存储数据,TreeBin封装了TreeNode,提供了读写锁;
3)get方法:计算hash值,如果定位到table本身,直接返回;如果不是,根据当前节点类型,分别按照链表和红黑树的方式去查找当前元素所在的位置
4)put方法:如果没有初始化,首先进行初始化;使用CAS无锁方式插入,如果发现需要扩容,首先进行扩容;如果存在hash冲突,需要挂在table节点下面,先将当前table节点加锁,链表按照尾插入方式进行插入,红黑树按照红黑树的结构进行插入,同时put在插入过程中,如果发现table里面的元素超过8个,就将链表改造成红黑树,并且还会进行元素个数的统计,并检查是否需要扩容;
5)扩容方法:1.8里面,为了提高效率,工作线程会进行并发扩容,同时为了避免多个线程有并发冲突,每个线程会进行步长的方式在节点之间来进行操作;
JDK 1.8 的 ConcurrentHashMap 是在 JDK 1.8 的 HashMap 上实现线程安全的。
存储结构采用 Node 数组 + 链表 + 红黑树 结构。省去了分段锁数组那一层,结构上与 HashMap 相同。
取消分段锁,通过 CAS + synchronized 来实现更细粒度的锁保护。这里的锁是指锁table的首个Node节点。在添加数据的时候,如果Node数组没有值的情况,则会使用CAS添加数据,CAS成功则添加成功,失败则进入锁代码块执行插入链表或红黑树或转红黑树操作。
2、ConcurrentHashMap中1.7和1.8的区别(*10)
JDK版本 | 1.7 | 1.8 |
---|---|---|
底层实现 | 数组+链表 | 数组+链表 / 红黑树 |
数据结构 | Segment 数组 + HashEntry 节点 | Node 节点 |
锁 | 分段锁,默认并发是16,一旦初始化,Segment 数组大小就固定,后面不能扩容 | Volatile+CAS + synchronized 来保证并发安全性(*4) |
put 操作 | 先获取锁,根据 key 的 hash 值 定位到 Segment ,再根据 key 的 hash 值 找到具体的 HashEntry ,再进行插入或覆盖,最后释放锁 | 根据 key 的 hash 值 定位到 Node节点,再判断首节点是否为空,空的话通过 cas 去赋值首节点 ; 首节点非空的话,会用 synchronized 去锁住首节点,并判断是是同个首节点,是的话再去操作 |
释放锁 | 需要显示调用 | unlock()不需要 |
追问1:1.8 做了什么优化?为什么这么做?( 2-2 ……)
3、ConcurrentHashMap的get()方法
- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
- 如果是红黑树那就按照树的方式获取值。
- 就不满足那就按照链表的方式遍历获取值。
追问1:ConcurrentHashMap的get方法是否要加锁,为什么?
不要,Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改因为hash冲突修改结点的val或者新增节点的时候是对线程B可见的。
4、ConcurrentHashMap的put()方法
- 根据 key 计算出 hashcode 。
- 判断是否需要进行初始化。
- 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败 则自旋保证成功。
- 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
- 如果都不满足,则利用 synchronized 锁写入数据。
- 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。
追问1:put()方法如何实现线程安全呢?……
追问2:Hashtable的 get、put 过程?
类似,只是没这么复杂,用synchronized修饰。
public synchronized V get(Object key) {
Entry