曹工说JDK源码(3)--ConcurrentHashMap,Hash算法优化、位运算揭秘
hashcode,有点讲究
什么是好的hashcode,一般来说,一个hashcode,一般用int来表示,32位。
下面两个hashcode,大家觉得怎么样?
0111 1111 1111 1111 1111 1111 1111 1111 ------A
1111 1111 1111 1111 1111 1111 1111 1111 ------B
只有第32位(从右到左)不一样,好像也没有所谓的好坏吧?
那,我们再想想,hashcode一般怎么使用呢?在hashmap中,由数组+链表+红黑树组成,其中,数组乃重中之重,假设数组长度为2的n次方,(hashmap的数组,强制要求长度为2的n次方),这里假设为8.
大家又知道,hashcode 对 8 取模,效果等同于 hashcode & (8 - 1)。
那么,前面的A 和 (8 - 1)相与的结果如何呢?
0111 1111 1111 1111 1111 1111 1111 1111 ------A
0000 0000 0000 0000 0000 0000 0000 0111 ------ 8 -1
相与
0000 0000 0000 0000 0000 0000 0000 0111 ------ 7
结果为7,也就是,会放进array[7]。
大家再看B的计算过程:
1111 1111 1111 1111 1111 1111 1111 1111 ------B
0000 0000 0000 0000 0000 0000 0000 0111 ------ 8 -1
相与
0000 0000 0000 0000 0000 0000 0000 0111 ------ 7
虽然B的第32位为1,但是,奈何和我们相与的队友,7,是个垃圾。
前面的高位,全是0。
ok,你懂了吗,数组长度太小了,才8,导致前面有29位都是0;你可能觉得一般容量不可能这么小,那假设容量为2的16次方,容量为65536,这下不是很小了吧,但即使如此,前面的16位也是0.
所以,问题明白了吗,我们计算出来的hashcode,低位相同,高位不同;但是,因为和我们进行与
计算的队友太过垃圾,导致我们出现了hash冲突。
ok,我们怎么来解决这个问题呢?
我们能不能把高位也参与计算呢?自然,是可以的。
hashmap中如何优化
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这里,其实分了3个步骤:
计算hashcode,作为操作数1
h = key.hashCode()
将第一步的hashcode,右移16位,作为操作数2
h >>> 16
操作数1 和 操作数2 进行异或操作,得到最终的hashcode
还是拿前面的来算,
0111 1111 1111 1111 1111 1111 1111 1111 ------A
0000 0000 0000 0000 0111 1111 1111 1111 ----- A >>> 16
异或(相同则为0,否则为1)
0111 1111 1111 1111 1000 0000 0000 0000 --- 2147450880
这里算出来的结果是 2147450880,再去对 7 进行与运算:
0111 1111 1111 1111 1000 0000 0000 0000 --- 2147450880
0000 0000 0000 0000 0000 0000 0000 0111 ------ 8 -1
与运算
0000 0000 0000 0000 0000 0000 0000 0000 ------ 0
这里的A,算出来,依然在array[0]。
再拿B来算一下:
1111 1111 1111 1111 1111 1111 1111 1111 ------ B
0000 0000 0000 0000 1111 1111 1111 1111 ----- B >>> 16
异或(相同则为0,否则为1)
1111 1111 1111 1111 0000 0000 0000 0000 --- -65536
0000 0000 0000 0000 0000 0000 0000 0111 ------ 7
与运算
0000 0000 0000 0000 0000 0000 0000 0000 ------- 0
最终算出来为0,所以,应该放在array[0]。
恩?算出来两个还是冲突了,我只能说,我挑的数字真的牛逼,是不是该去买彩票啊。。
总的来说,大家可以多试几组数,下边提供下源代码:
public class BinaryTest {
public static void main(String[] args) {
int a = 0b00001111111111111111111111111011;
int b = 0b10001101111111111111110111111011;
int i = tabAt(32, a);
System.out.println("index for a:" + i);
i = tabAt(32, b);
System.out.println("index for b:" + i);
}
static final int tabAt(int arraySize, int hash) {
int h = hash;
int finalHashCode = h ^ (h >>> 16);
int i = finalHashCode & (arraySize - 1);
return i;
}
}
虽然说,我测试了几个数字,还是有些冲突,但是,你把高16位弄进来参与计算,总比你不弄进来计算要好吧。
大家也可以看看hashmap中,hash方法的注释:
/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
里面提到了2点:
So we apply a transform that spreads the impact of higher bits downward.
所以,我们进行了一个转换,把高位的作用利用起来。
we just XOR some shifted bits in the cheapest possible way to reduce systematic lossage, as well as
to incorporate impact of the highest bits that would otherwise never be used in index calculations because of table bounds.
我们仅仅异或了从高位移动下来的二进制位,用最经济的方式,削减系统性能损失,同样,因为数组大小的限制,导致高位在索引计算中一直用不到,我们通过这种转换将其利用起来。
ConcurrentHashMap如何优化
在concurrentHashMap中,其主要是:
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
这里主要是使用spread方法来计算hash值:
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
大家如果要仔细观察每一步的二进制,可以使用下面的demo:
static final int spread(int h) {
// 1
String s = Integer.toBinaryString(h);
System.out.println("h:" + s);
// 2
String lower16Bits = Integer.toBinaryString(h >>> 16);
System.out.println("lower16Bits:" + lower16Bits);
// 3
int temp = h ^ (h >>> 16);
System.out.println("h ^ (h >>> 16):" + Integer.toBinaryString(temp));
// 4
int result = (temp) & HASH_BITS;
System.out.println("final:" + Integer.toBinaryString(result));
return result;
}
这里和HashMap相比,多了点东西,也就是多出来了:
& HASH_BITS;
这个有什么用处呢?
因为(h ^ (h >>> 16))
计算出来的hashcode,可能是负数。这里,和 HASH_BITS进行了相与:
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
1111 1111 1111 1111 1111 1111 1111 1111 假设计算出来的hashcode为负数,因为第32位为1
0111 1111 1111 1111 1111 1111 1111 1111 0x7fffffff
进行相与
0111 ..................................
这里,第32位,因为0x7fffffff的第32位,总为0,所以相与后的结果,第32位也总为0 ,所以,这样的话,hashcode就总是正数了,不会是负数。
concurrentHashMap中,node的hashcode,为啥不能是负数
当hashcode为正数时,表示该哈希桶为正常的链表结构。
当hashcode为负数时,有几种情况:
ForwardingNode
此时,其hash值为:
static final int MOVED = -1; // hash for forwarding nodes
当节点为ForwardingNode类型时(表示哈希表在扩容进行中,该哈希桶已经被迁移到了新的临时hash表,此时,要get的话,需要去临时hash表查找;要put的话,是不行的,会帮助扩容)
TreeBin
static final int TREEBIN = -2; // hash for roots of trees
表示,该哈希桶,已经转了红黑树。
扩容时的位运算
/**
* Returns the stamp bits for resizing a table of size n.
* Must be negative when shifted left by RESIZE_STAMP_SHIFT.
*/
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
这里,假设,n为4,即,hashmap中数组容量为4.
下面这句,求4的二进制表示中,前面有多少个0.
Integer.numberOfLeadingZeros(n)
表示为32位后,如下
0000 0000 0000 0000, 0000 0000 0000 0100
所以,前面有29个0,即,这里的结果为29.
(1 << (RESIZE_STAMP_BITS - 1)
这一句呢,其中RESIZE_STAMP_BITS 是个常量,为16. 相当于,把1 向左移动15位。
二进制为:
1000 0000 0000 0000 -- 1 << 15
最终结果:
0000 0000 0000 0000 0000 0000 0001 1101 -- 29
0000 0000 0000 0000 1000 0000 0000 0000 -- 1 << 15
进行或运算
0000 0000 0000 0000 1000 0000 0001 1101 -- 相当于把29的第一位,变成了1,其他都没变。
所以,最终结果是,
这个数,换算为10进制,为32972,是个正数。
这个数,有啥用呢?
在addCount函数中,当整个哈希表的键值对数量,超过sizeCtl时(一般为0.75 * 数组长度),就会触发扩容。
java.util.concurrent.ConcurrentHashMap#addCount
int sc = sizeCtl;
boolean bSumExteedSizeControl = newBaseCount >= (long) sc;
// 1
if (bContinue) {
int rs = resizeStamp(n);
// 2
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 3
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
newBaseCount = sumCount();
} else {
break;
}
1处,如果扩容条件满足
2处,如果sc小于0,这个sc是啥,就是前面说的sizeCtl,此时应该是等于:0.75 * 数组长度,不可能为负数
3处,将sc(此时为正数),cas修改为:
(rs << RESIZE_STAMP_SHIFT) + 2)
这个数有点意思了,rs就是前面我们的resizeStamp得到的结果。
按照前面的demo,我们拿到的结果为:
0000 0000 0000 0000 1000 0000 0001 1101 -- 相当于把29的第一位,变成了1,其他都没变。
因为
private static int RESIZE_STAMP_BITS = 16;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
所以,RESIZE_STAMP_SHIFT 为16.
0000 0000 0000 0000 1000 0000 0001 1101 -- 相当于把29的第一位,变成了1,其他都没变。
1000 0000 0001 1101 0000 0000 0000 0000 --- 左移16位,即 rs << RESIZE_STAMP_SHIFT
1000 0000 0001 1101 0000 0000 0000 0010 -- (rs << RESIZE_STAMP_SHIFT) + 2)
最终,这个数,第一位是 1,说明了,这个数,肯定是负数。
大家如果看过其他人写的资料,也就知道,当sizeCtl为负数时,表示正在扩容。
所以,这里
if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
这句话就是,如果当前线程成功地,利用cas,将sizeCtl从正数,变成负数,就可以进行扩容。
扩容时,其他线程怎么执行
// 1
if (bContinue) {
int rs = resizeStamp(n);
// 2
if (sc < 0) {
// 2.1
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 2.2
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 3
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
newBaseCount = sumCount();
} else {
break;
}
此时,因为上面的线程触发了扩容,sc已经变成了负数了,此时,新的线程进来,会判断2处。
2处是满足的,会进入2.1处判断,这里的部分条件看不懂,大概是:扩容已经结束,就不再执行,直接break
否则,进入2.2处,辅助扩容,同时,把sc变成sc + 1,增加扩容线程数。
总结
时间仓促,如有问题,欢迎指出。
曹工说JDK源码(3)--ConcurrentHashMap,Hash算法优化、位运算揭秘的更多相关文章
- 曹工说JDK源码(4)--抄了一小段ConcurrentHashMap的代码,我解决了部分场景下的Redis缓存雪崩问题
曹工说JDK源码(1)--ConcurrentHashMap,扩容前大家同在一个哈希桶,为啥扩容后,你去新数组的高位,我只能去低位? 曹工说JDK源码(2)--ConcurrentHashMap的多线 ...
- 曹工说JDK源码(2)--ConcurrentHashMap的多线程扩容,说白了,就是分段取任务
前言 先预先说明,我这边jdk的代码版本为1.8.0_11,同时,因为我直接在本地jdk源码上进行了部分修改.调试,所以,导致大家看到的我这边贴的代码,和大家的不太一样. 不过,我对源码进行修改.重构 ...
- 曹工说JDK源码(1)--ConcurrentHashMap,扩容前大家同在一个哈希桶,为啥扩容后,你去新数组的高位,我只能去低位?
如何计算,一对key/value应该放在哪个哈希桶 大家都知道,hashmap底层是数组+链表(不讨论红黑树的情况),其中,这个数组,我们一般叫做哈希桶,大家如果去看jdk的源码,会发现里面有一些变量 ...
- 曹工说Redis源码(2)-- redis server 启动过程解析及简单c语言基础知识补充
文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读.由于 ...
- 曹工说Redis源码(3)-- redis server 启动过程完整解析(中)
文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读.由于 ...
- 曹工说Redis源码(4)-- 通过redis server源码来理解 listen 函数中的 backlog 参数
文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读.由于 ...
- 曹工说Redis源码(5)-- redis server 启动过程解析,以及EventLoop每次处理事件前的前置工作解析(下)
曹工说Redis源码(5)-- redis server 启动过程解析,eventLoop处理事件前的准备工作(下) 文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis ...
- 曹工说Redis源码(6)-- redis server 主循环大体流程解析
文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读.由于 ...
- 曹工说Redis源码(7)-- redis server 的周期执行任务,到底要做些啥
文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读.由于 ...
随机推荐
- React实践相关
语法高亮: sublime ctrl+shift+P 安装babel ,在view-syntax-open all width current extension as...-babel-js(bab ...
- Codeforces1157A(A题)Reachable Numbers
A. Reachable Numbers Let's denote a function f(x)f(x) in such a way: we add 11 to xx, then, while th ...
- 解决删除~/Library/Caches/CocoaPods/search_index.json重新pod search还是不起作用
今天新苹果机安装cocoapods,安装完以后发现怎么pod search 都没有用 命令行提示: swhcxp@iosdevmac ~ % pod search Almofire Setup com ...
- 初探Redis-基础类型List
Redis存在五种基础类型:字符串(String).列表(List).哈希(Hash).集合(Set).有序集合(Sorted Set).本次列举出List的常用操作. Redis官网:https:/ ...
- nodejs链接mysql 中的问题
首先你得对mysql ,有个大概的认识. 比如说:如何安装,使用基本的语法,测试安装是否能成功,以及成功之后简单的对于数据库的,操作(增删改查)... 下面是业务场景:在爬虫过程中,租后需要将信息输出 ...
- 【NLP】常用优化方法
目录 梯度下降法 动量法 AdaGrad算法 RMSProP算法 AdaDelta算法 Adam算法 1.梯度下降法 梯度下降法可以分为三种,批量梯度下降法(BGD).小批量梯度下降(MBGD).随机 ...
- .NET CORE 依赖注入 实践总结
知识点回顾 依赖包. Microsoft.Extensions.DependencyInjection.Abstractions 核心对象和方法. IServiceCollection.注入对象的容器 ...
- flutter 环境出错后排查
莫名其妙地环境坏了 VSCode 终端里执行 flutter run 卡在 installing.. 模拟器上闪了一下,打不开, 应该是安装出错爆掉了 flutter doctor 检查一下: X A ...
- 化学元素周期表的英文全称 Periodic Table of the Elements
化学元素周期表的英文全称 Periodic Table of the Elements 缩写 PTE 拉丁文 英文 1 H 氢 Hydrogenium Hydrogen 2 He 氦 Helium ...
- 几种常见的app推广形式
*:first-child { margin-top: 0 !important; } body > *:last-child { margin-bottom: 0 !important; } ...