hash
把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。
常用HASH 函数:直接取余法、乘法取整法、平方取中法。
处理冲突方法:
- 开放寻址法
- 再散列法
- 链地址法(拉链法)
ConcurrentHashMap
1.7 中HashMap 死循环分析
在多线程环境下,使用HashMap 进行put 操作会引起死循环,导致CPU 利用率接近100%,HashMap 在并发执行put 操作时会引起死循环,是因为多线程会导致HashMap 的Entry 链表形成环形数据结构,一旦形成环形数据结构,Entry 的next 节点永远不为空,就会产生死循环获取Entry。
HashMap 一次扩容的过程:
- 取当前table的2倍作为新table 的大小
- 根据算出的新table 的大小new 出一个新的Entry 数组来,名为newTable
- 轮询原table 的每一个位置,将每个位置上连接的Entry,算出在新table上的位置,并以链表形式连接
- 原table上的所有Entry全部轮询完毕之后,意味着原table 上面的所有Entry 已经移到了新的table上,HashMap中的table 指向newTable

HashMap 之所以在并发下的扩容造成死循环,是因为,多个线程并发进行时,因为一个线程先期完成了扩容,将原Map 的链表重新散列到自己的表中,并且链表变成了倒序,后一个线程再扩容时,又进行自己的散列,再次将倒序链表变为正序链表。于是形成了一个环形链表,当get 表中不存在的元素时,造成死循环。
putIfAbsent
ConcurrentHashMap还提供了一个在并发下比较有用的方法putIfAbsent,如果传入key 对应的value已经存在,就返回存在的value,不进行替换。如果不存在,就添加key 和value,返回null。
synchronized(map){
if (map.get(key) == null){
return map.put(key, value);
} else{
return map.get(key);
}
}
ConcurrentHashMap 实现分析
1.7下的实现


ConcurrentHashMap 是由Segment 数组结构和HashEntry 数组结构组成。Segment 是一种可重入锁(ReentrantLock),在ConcurrentHashMap 里扮演锁的角色;HashEntry 则用于存储键值对数据。一个ConcurrentHashMap 里包含一个Segment 数组。Segment 的结构和HashMap 类似,是一种数组和链表结构。一个Segment 里包含一个HashEntry 数组,每个HashEntry 是一个链表结构的元素,每个Segment 守护着一个HashEntry 数组里的元素,当对HashEntry 数组的数据进行修改时,必须首先获得与它对应的Segment 锁。
构造方法和初始化

ConcurrentHashMap 初始化方法是通过initialCapacity、loadFactor 和concurrencyLevel(参数concurrencyLevel 是用户估计的并发级别,就是说你觉得最多有多少线程共同修改这个map,根据这个来确定Segment 数组的大小concurrencyLevel 默认是DEFAULT_CONCURRENCY_LEVEL = 16;)等几个参数来初始化segment 数组、段偏移量segmentShift、段掩码segmentMask 和每个segment里的HashEntry 数组来实现的。
ConcurrentHashMap 默认的并发度为16,但用户也可以在构造函数中设置并发度。当用户设置并发度时,ConcurrentHashMap 会使用大于等于该值的最小2 幂指数作为实际并发度。
get 操作

get 操作先经过一次再散列,然后使用这个散列值通过散列运算定位到Segment(使用了散列值的高位部分),再通过散列算法定位到table(使用了散列值的全部)。整个get 过程,没有加锁,而是通过volatile 保证get 总是可以拿到最新值。

put 操作

ConcurrentHashMap 初始化的时候会初始化第一个槽segment[0],对于其他槽,在插入第一个值的时候再进行初始化。ensureSegment 方法考虑了并发情况,多个线程同时进入初始化同一个槽segment[k],但只要有一个成功就可以了。

rehash 操作
扩容是新创建了数组,然后进行迁移数据,最后再将newTable 设置给属性table。
在1.8 下的实现
改进
改进一:取消segments 字段,直接采用transient volatile HashEntry<K,V>[]table 保存数据,采用table 数组元素作为锁,从而实现了对缩小锁的粒度,进一步减少并发冲突的概率,并大量使用了采用了CAS + synchronized 来保证并发安全性。
改进二:将原先table 数组+单向链表的数据结构,变更为table 数组+单向链表+红黑树的结构。对于hash 表来说,最核心的能力在于将key hash 之后能均匀的分布在数组中。如果hash 之后散列的很均匀,那么table 数组中的每个队列长度主要为0 或者1。但实际情况并非总是如此理想,虽然ConcurrentHashMap 类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8 中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。
用于判断是否需要将链表转换为红黑树的阈值

用于判断是否需要将红黑树转换为链表的阈值


核心数据结构和属性
Node
Node 是最核心的内部类,它包装了key-value 键值对。

TreeNode
树节点类,另外一个核心的数据结构。当链表长度过长的时候,会转换为TreeNode。

与1.8 中HashMap 不同点:
1、它并不是直接转换为红黑树,而是把这些结点放在TreeBin 对象中,由TreeBin 完成对红黑树的包装。
2、TreeNode 在ConcurrentHashMap 扩展自Node 类,而并非HashMap 中的扩展自LinkedHashMap.Entry<K,V>类,也就是说TreeNode 带有next 指针。
TreeBin
负责TreeNode 节点。它代替了TreeNode 的根节点,也就是说在实际的ConcurrentHashMap“数组”中,存放的是TreeBin 对象,而不是TreeNode 对象。另外这个类还带有了读写锁机制。

特殊的ForwardingNode
一个特殊的Node 结点,hash 值为-1,其中存储nextTable 的引用。有table 发生扩容的时候,ForwardingNode 发挥作用,作为一个占位符放在table中表示当前结点为null 或者已经被移动。
sizeCtl
用来控制table 的初始化和扩容操作。
负数代表正在进行初始化或扩容操作
- -1 代表正在初始化
- -N 表示有N-1 个线程正在进行扩容操作0 为默认值,代表当时的table 还没有被初始化正数表示初始化大小或Map 中的元素达到这个数量时,需要进行扩容了。

get 操作
get 方法比较简单,给定一个key 来确定value 的时候,必须满足两个条件key 相同hash 值相同,对于节点可能在链表或树上的情况,需要分别去查找。

put 操作



总结来说,put 方法就是,沿用HashMap 的put 方法的思想,根据hash 值计算这个新插入的点在table 中的位置i,如果i 位置是空的,直接放进去,否则进行判断,如果i 位置是树节点,按照树的方式插入新的节点,否则把i 插入到链表的末尾。
如果加入这个节点以后链表长度大于8,就把这个链表转换成红黑树。如果这个节点的类型已经是树节点的话,直接调用树节点的插入方法进行插入新的值。
初始化
前面说过,构造方法中并没有真正初始化,真正的初始化在放在了是在向ConcurrentHashMap 中插入元素的时候发生的。具体实现的方法就是initTable

HashTable
HashTable 容器使用synchronized 来保证线程安全,但在线程竞争激烈的情况下HashTable 的效率非常低下。因为当一个线程访问HashTable 的同步方法,其他线程也访问HashTable 的同步方法时,会进入阻塞或轮询状态。如线程1 使用put 进行元素添加,线程2 不但不能使用put 方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。
3404

被折叠的 条评论
为什么被折叠?



