简单了解HashMap和ConcurrentHashMap
Map
HashMap
jdk1.7与jdk1.8的区别
- 底层数据结构,jdk1.7为数组 + 链表,jdk1.8为数组 + 链表 + 红黑树
- 链表添加元素的位置,jdk1.7为头插法(循环引用问题),jdk1.8为尾插法
- 对于key==null的处理,jdk1.7为添加前独立判断,jdk1.8为综合到hash()函数里面
- 先添加再扩容还是先扩容再添加。jdk1.7为先扩容再添加,jdk1.8为先添加再扩容
- hash()函数的异或次数,jdk1.7为4次,jdk1.8为1次(高16位和低16位异或)
jdk1.8要比jdk1.7多一步考虑红黑树的操作
- 比如插入考虑树化,删除考虑反树化,查找考虑树的递归查找
- 扩容的时候会把每个索引位置后边的链表或者红黑树都拆分成两个部分(i、i + oldcap),比如 1 和 17 在容量为 16 的hashmap都是在索引为1的位置,扩容后 1 还是在索引为 1 的位置,但是 17 到了索引为 17 的位置
jdk1.7实现
底层是数组 + 链表
1 | static class Entry<K,V> implements Map.Entry<K,V> { |
初始化
调用inflateTable()方法进行初始化
初始数组大小默认为16个,负载因子为3/4,数组达到负载因子则进行扩容
hash()函数
4次异或
put()
- 如果table[]为空,创建table[]数组
- 如果key == null,会在下标为0位置放入
- 通过hash函数计算key的hashCode的hash值
- 通过hash值和数组长度-1进行&运算(取模运算),得到key在哪个桶
- 如果桶内没有元素,创建新节点,直接插入
- 否则判断首元素是否key相同,是的话,进行 value 替换
- 否则,进行链表查找插入,如果找到了key相同的元素,进行value替换
- 否则插入元素,如果总节点数到达阈值64,先扩容,再头插法添加元素
1 | public V put(K key, V value) { |
get()
- 如果key == null,会在下标为0位置查询
- 通过hash函数计算出key的hashCode的hash值
- 如果桶内位置首元素符合,返回结果
- 否则遍历链表
- 找不到返回null
1 | public V get(Object key) { |
扩容resize()
- 数组扩容2倍
- 调用transfer()函数,遍历所有桶,把每个桶内所有的元素重新rehash到新的数组
- 更新扩容阈值
1 | void resize(int newCapacity) { |
死链问题
扩容的时候线程并发产生的循环引用,主要是前插法同时线程并发导致的
1 | void transfer(Entry[] newTable, boolean rehash) { |
数据覆盖问题
比如A线程判断某个桶内首元素为NULL,准备插入的时候挂起,这时B线程完成插入,等A线程回来,会覆盖掉B线程插入的数据
1 | if ((p = tab[i = (n - 1) & hash]) == null) |
jdk1.8实现
数组 + 链表 + 红黑树
Node结构
1 | static class Node<K,V> implements Map.Entry<K,V> { |
TreeNode树节点的结构
1 | static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { |
初始化
调用resize()方法创建table[]数组
初始数组大小默认为16个,负载因子为3/4,数组达到负载因子则进行扩容
hash()函数
取hashCode的高低16位互相异或
put()
- 通过hash函数计算出key的hashCode的hash值
- 如果table[]为空,通过resize()方法创建table[]数组
- 通过hash值和数组长度-1进行&运算,得到key在哪个桶
- 桶为空,创建新节点,直接插入
- 否则判断首元素是否key相同,是的话,进行 value 替换
- 否则判断首元素是否是TreeNode,是的话,红黑树查询插入
- 否则,进行链表查询插入,如果中途找到了key相同的元素,那么进行value替换
- 否则添加元素,添加完之后判断是否需要树化
- ++size > threshold,判断是否需要扩容
1 | public V put(K key, V value) { |
红黑树插入putTreeVal()
根据hash值来排序,放左边或者右边
1 | final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, |
get()
- 通过hash函数计算出key的hashCode的hash值
- 如果桶内首元素符合,返回结果
- 首元素是否是TreeNode,是的话,红黑树搜索
- 否则遍历链表
- 找不到返回null
1 | public V get(Object key) { |
红黑树搜索getTreeNode()
1 | final TreeNode<K,V> getTreeNode(int h, Object k) { |
扩容resize()
如果是初始化,会默认初始化容量和加载因子
正常扩容的话
数组两倍扩容
遍历所有桶,重新rehash桶内所有的元素
当前桶内位置的首元素后面没有链表或者树,直接插入到新数组
首元素后面是红黑树的话,执行split()函数,遍历树所有节点,分成两部分,如果某个部分元素个数<=6,那么退化为链表
首元素后面是链表的话,根据e.hash & oldCap == 0,将链表分成lo,hi两个链表,一点点优化(不用rehash),分别放在下标为 j 和 j + oldCap位置
1 | final Node<K,V>[] resize() { |
树化treeifyBin()
- 遍历原链表,把Node转为TreeNode,来创建一个TreeNode链表
- 遍历TreeNode链表,把每个TreeNode加入到红黑树当中
- 没有根节点,直接创建新的根节点,颜色为黑色
- 否则从根节点开始遍历,对比当前节点和叶子节点的hash值得出不同的dir,判断叶子节点插入当前节点左边(-1)还是右边(1),并且要进行左旋或者右旋,来维护红黑树的特性
- 确保给定的根节点是所在桶的第一个节点
1 | final void treeifyBin(Node<K,V>[] tab, int hash) { |
取消树化untreeify()
TreeNode转Node,树转成链表
1 | final Node<K,V> untreeify(HashMap<K,V> map) { |
维护红黑树特性balanceInsertion()
可能涉及左旋或者右旋
1 | static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, |
左旋右旋
以下红黑树部分参考HashMap源码分析putTreeVal(红黑树部分)——java学习总结(11)-CSDN博客
插入的节点一开始默认都为红色,左左(指的是父节点是祖父节点的左孩子,自己是父节点的左孩子)
- 无需调整:父节点是黑色
- 变色:父节点是红色并且叔父节点也是红色
- 变色 + 旋转:父节点是红色,叔父节点是黑色
无需调整 | 【变色】即可实现平衡 | 【旋转+变色】才可实现平衡 |
---|---|---|
(1)当父节点为黑色时直接插入子节点 | (2)空树插入根节点,将根节点红色变为黑色 | (4)父节点为红色左节点,叔父节点为黑色,插入左子节点,那么通过【左左节点旋转】 |
(3)父节点和叔父节点都为红色,则将父节点和叔父结点置为黑色,再将祖父结点置为红色,再递归向上处理(将祖父结点视为新插入的结点,将父节点和叔父节点看作黑色的null) | (5)父节点为红色左节点,叔父节点为黑色,插入右子节点,那么通过【左右节点旋转】 | |
(6)父节点为红色右节点,叔父节点为黑色,插入左子节点,那么通过【右左节点旋转】 | ||
(7)父节点为红色右节点,叔父节点为黑色,插入右子节点,那么通过【右右节点旋转】 |
(4)左左节点旋转
(5)左右节点旋转
(6)右左节点旋转
(7)右右节点旋转
数据覆盖问题
比如A线程判断某个桶内首元素为NULL,准备插入的时候挂起,这时B线程完成插入,等A线程回来,会覆盖掉B线程插入的数据
1 | if ((p = tab[i = (n - 1) & hash]) == null) |
ConcurrentHashMap
ConcurrentHashMap是线程并发安全的hashmap,所以他会解决hashmap的一些线程问题(使用CAS或者锁),比如jdk1.7的循环引用问题或者说是数据覆盖问题
jdk1.7与jdk1.8的区别
jdk1.7可以理解为hashmap拆成了很多份,把每个小的hashmap锁起来了,只有获取到锁才能写这个小的hashmap,读不受影响
jdk1.8沿用了原生的hashmap,只不过在写操作的时候,索引首元素用CAS,首元素后面的插入会用synchronized锁起来
- 数据结构:取消了 Segment 分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
- 保证线程安全机制:JDK1.7 采用 Segment 的分段锁机制实现线程安全,其中 Segment 继承自 CAS + ReentrantLock 。JDK1.8 采用CAS + synchronized保证线程安全。
- 锁的粒度降低:JDK1.7 是对需要进行数据操作的 Segment 加锁,JDK1.8 调整为对每个数组元素加锁(Node)。
jdk1.7实现
segment数组 + hashEntry数组 + 链表
初始化
- 初始化segment数组的大小,默认为16,如果不是2^n,则向上取整(比如segment数组的大小传入10,其实还是16)
- 初始化entry数组的大小,默认是2,同样需要向上取整,为什么要向上取整,因为hash函数要与操作
- 然后segment数组首个元素先创造一个segment对象,方便之后put()快速创建新的segment对象
1 | // initialCapacity是entry数组的初始容量 |
put()
通过第一次hash函数(hashcode和segment数组长度减一进行与运算)算出key对应的segment数组的下标
判断这个位置是否需要创建segment对象,如果需要,获取segment[0]的信息可以快速创建segment对象,通过CAS保证创建segment对象是并发安全的
trylock()尝试上锁,获取锁成功才能插入元素,否则进入自旋锁操作(期间可以遍历链表,看看是否要创建新元素,如果别的线程插入修改了链表的头节点,就去重新遍历链表,如果自旋次数太多就阻塞)
获取锁成功的话
1 | private Segment<K,V> ensureSegment(int k) { |
1 | final V put(K key, int hash, V value, boolean onlyIfAbsent) { |
获取锁的自旋操作scanAndLockForPut()
先做一些准备工作,直到获取锁
- 准备个新节点,找到它的位置
- 获取锁一直失败导致循环次数太多,那么直接lock()阻塞
- 偶数次循环的时候如果发现链表头节点变化,那么重新遍历
1 | private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) { |
get()
无锁,因为Node的value和next是用volatile修饰了,其他线程的修改对本线程可见
1 | public V get(Object key) { |
扩容机制rehash()
segment数组大小初始化之后不会改变,hashentry数组的扩容和hashMap一样
遍历一遍链表,找到一个lastRun/lastIdx(通过hashCode & oldtable.length看1、0),这个节点以后都是要rehash到相同位置的
1 | private void rehash(HashEntry<K,V> node) { |
remove()
1 | final V remove(Object key, int hash, Object value) { |
jdk1.8实现
node数组 + 链表 + 红黑树
初始化
如果node数组为空,利用CAS创建node数组,默认大小为16个,默认扩展因子是0.75
put()
- 根据 key 计算出 hash
- 判断node数组是否已经初始化,没有就初始化node数组
- hash出下标,如果为空,CAS尝试写入
- 如果当前位置的
hash == MOVED == -1
,则需要进行扩容 - 如果都不满足,加synchronized锁,将entry加入链表或者红黑树
- 判断是否需要树化
1 | private final Node<K,V>[] initTable() { |
1 | final V putVal(K key, V value, boolean onlyIfAbsent) { |
get()
无锁,因为Node的value和next是用volatile修饰了,其他线程的修改对本线程可见
- 根据key计算出hash
- 查找到指定位置,如果头节点是key相同,直接返回
- 如果hash<0,说明在扩容或者是红黑树,查找红黑树
- 否则查找链表
1 | public V get(Object key) { |
扩容机制transfer()
运用到了多线程,每个线程默认处理16个桶
ForwardingNode结构表示这个节点开头的那部分节点们正在迁移,用CAS操作让线程去取得迁移这个部分节点的权力
当所有节点迁移完成,finish = true
1 | private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { |
remove()
1 | final V replaceNode(Object key, V value, Object cv) { |