学习目标
了解HashMap 1.7 整体结构,从put get 操作了解HashMap 工作流程,及其存在的问题。
整体结构
从一个算法题讲起
如何得到大于或者等于某个正数的2的幂次方数?
public int getBiggerOrEqual(int size){
//假设size 是 大于0的
// 大致分为 2 类数据 一类是本身就是符合要求的2 的幂次方数 另一类就是不符合 其二进制数有多个bit 为1
if(size == 1){
return 1;
}
return Integer.highestOneBit(((size - 1 )<< 1));
}
jdk8的做法是
public int getBiggerOrEqual(int size){
size = size - 1;
size |= size >>> 1;
size |= size >>> 2;
size |= size >>> 4;
size |= size >>> 8;
size |= size >>> 16;
//通过上面的移位以及 或 的操作 此时 size 最高位之后变为全 1 的
return size + 1 ;
}
思考:jdk 在这方面类似的做法是 CurrentHashMap 在计算总数(不准确)的时候使用了LongAdder 的思想,但是同样没有使用封装好的代码 而是根据场景进行了适配。
hashSeed 有什么作用
通过Holder 来持有一个在VM 启动后才初始化的变量ALTERNATIVE_HASHING_THRESHOLD,该变量是一个替换哈希函数的阈值。该值可以通过我们 java -Djdk.map.althashing.threshold=512 MyApplication 来设置,之后扩容时我们的capacity 大于等于512 就可能发生rehash。
private static class Holder {
/**
* Table capacity above which to switch to use alternative hashing.
*/
static final int ALTERNATIVE_HASHING_THRESHOLD;
static {
String altThreshold = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
"jdk.map.althashing.threshold"));
int threshold;
try {
threshold = (null != altThreshold)
? Integer.parseInt(altThreshold)
: ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;
// disable alternative hashing if -1
if (threshold == -1) {
threshold = Integer.MAX_VALUE;
}
if (threshold < 0) {
throw new IllegalArgumentException("value must be positive integer.");
}
} catch(IllegalArgumentException failed) {
throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
}
ALTERNATIVE_HASHING_THRESHOLD = threshold;
}
}
final boolean initHashSeedAsNeeded(int capacity) {
boolean currentAltHashing = hashSeed != 0;
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
总的来说就是为了优化大量哈希碰撞下的哈希函数。The alternative hash function improves the performance of these map implementations when a large number of key hash collisions are encountered.具体可参考oracle se7 doc
分析多线程环境下resize成环问题
JDK1.7的特点
整体结构:数组+单链表
扩容时机 :当插入元素时需要新建entry 时候,若当前size >= threshold && null != table[index] 时候才扩容。扩容后元素通过 (哈希值) & (tableSize - 1)放置元素,若没有经过rehash 则元素是只能放到两个桶位位置,一个是低桶位(新增参与运算的位 0)一个是高桶位(新增参与运算的位为1) 二者差值 为旧table的length
扩容前后
数组长度 - 1
key (新增参与运算的位 的情况)
key 对应的下标值
扩容前(容量 16)
0000 0000 0000 0000 0000 0000 0000 1111
1101 1111 1110 11111 11111 1000 1111 1010
0000 0000 0000 0000 0000 0000 0000 1010(10)
扩容后(容量32)
0000 0000 0000 0000 0000 0000 0001 1111
1101 1111 1110 11111 11111 1000 1111 1010
0000 0000 0000 0000 0000 0000 0001 1010(26)
插入元素时采用的方式:头插法。原因:刚插入的数据更容易被使用。
先扩容后插入
hash函数:在key 为String 并且hashSeed != 0 情况下使用优化的hash函数,否则经过多次移位扰动。
loadfactor : 作为一条通用的规则,loadfactor 设置为0.75.这是时间和空间的权衡选择,要是特别大那么可能存在这样的情况:hash冲突特别严重但是就是没达到阈值,而如果小点的话就能减轻这种情况。所以 loadfacor 特别大应该会影响get 和put 操作的时间复杂度。特别小的时候又会造成空间的浪费。
“collection views method” 会fail fast 抛出并发修改异常。