本文主要讨论jdk1.7下hashMap的源码实现,其中主要是在扩容时容易出现死循环的问题,以及put元素的整个过程。java
数组+链表
示例图以下:算法
常量属性数组
/** * The default initial capacity - MUST be a power of two. * 默认初始容量大小 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * MUST be a power of two <= 1<<30. * hashMap最大容量,可装元素个数 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * The load factor used when none specified in constructor. * 加载因子,如容量为16,默认阈值即为16*0.75=12,元素个数超过(包含)12且,扩容 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 空数组,默认数组为空,初始化后才才有内存地址,第一次put元素时判断,延迟初始化 */ static final Entry<?,?>[] EMPTY_TABLE = {};
扩容致使的死循环,jdk1.7中在多线程高并发环境容易出死循环,致使cpu使用率太高问题,问题出在扩容方法resize()中,更具体内部的transfer方法:将旧数组元素转移到新数组过程当中,源码以下:安全
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; //1.若是原来数组容量等于最大值了,2^30,设置扩容阈值为Integer最大值,不须要再扩容 if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } //2.建立新数组对象 Entry[] newTable = new Entry[newCapacity]; //3.将旧数组元素转移到新数组中,分析一 transfer(newTable, initHashSeedAsNeeded(newCapacity)); //4.从新引用新数组对象和计算新的阈值 table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
transfer方法多线程
/** * Transfers all entries from current table to newTable. * 从当前数组中转移全部的节点到新数组中 */ void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; //遍历旧数组 for (Entry<K,V> e : table) { //1,首先获取数组下标元素 while(null != e) { //2.获取数组该桶位置链表中下一个元素 Entry<K,V> next = e.next; //3.是否须要从新该元素key的hash值 if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } //4,从新肯定在新数组中下标位置 int i = indexFor(e.hash, newCapacity); //5.头插法:插入新链表该桶位置,如有元素,就造成链表,每次新加入的节点都插在第一位,就数组下标位置 e.next = newTable[i]; newTable[i] = e; //6.继续获取链表下一个元素 e = next; } } } //传入容量值返回是否须要对key从新Hash final boolean initHashSeedAsNeeded(int capacity) { //1.hashSeed默认为0,所以currentAltHashing为false boolean currentAltHashing = hashSeed != 0; //2,sun.misc.VM.isBooted()在类加载启动成功后,状态会修改成true // 所以变数在于,capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD,debug发现正常状况ALTERNATIVE_HASHING_THRESHOLD是一个很大的值,使用的是Integer的最大值 boolean useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); //3,二者异或,只有不相同时才为true,即useAltHashing =true时,dubug代码发现useAltHashing =false, boolean switching = currentAltHashing ^ useAltHashing; if (switching) { hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this) : 0; } //正常状况下是返回false,即不须要从新对key哈希 return switching; }
上面源码展现转移元素过程:并发
如下模拟2个线程并发操做hashMap 在put元素时形成的死循环过程:app
链表死循环图例:dom
1.7的put方法,因没有红黑树结构,相比较1.8简单, 容易理解,流程图以下所示:函数
代码以下:高并发
public V put(K key, V value) { //1,若当前数组为空,初始化 if (table == EMPTY_TABLE) { //分析1 inflateTable(threshold); } //2,若put的key为null,在放置在数组下标第一位,索引为0位置,从该源码可知 // hashMap容许 键值对 key=null,可是只能有惟一一个 if (key == null) // 分析2 return putForNullKey(value); //3,计算key的hash,这里与1.8有区别 //分析3 int hash = hash(key); // 4,肯定在数组下标位置,与1.8相同 int i = indexFor(hash, table.length); // 5,遍历该数组位置,即该桶处遍历 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { // 找到相同的key,则覆盖原value值,返回旧值 V oldValue = e.value; e.value = value; //该方法为空,不用看 e.recordAccess(this); return oldValue; } } //由于hashMap线程不安全,修改操做没有同步锁, //该字段值用于记录修改次数,用于快速失败机制 fail-fast,防止其余线程同时作了修改,抛出并发修改异常 modCount++; // 6,原数组中没有相同的key,以头插法插入新的元素 //分析4 addEntry(hash, key, value, i); return null; }
分析1: HashMap如何初始化数组的,延迟初始化有什么好处?
结论: 一、1.7,1.8都是延迟初始化,在put第一个元素时建立数组,目的是为了节省内存。
初始化代码:
private void inflateTable(int toSize) { // Find a power of 2 >= toSize //1.该方法很是重要,目的为了获得一个比toSize最接近的2的幂次方的数, // 且该数要>=toSize,这个2的幂次方方便后面各类位运算 // 如:new HashMap(15),指定15大小集合,内部实际 建立数组大小为2^4=16 // 分析见下 int capacity = roundUpToPowerOf2(toSize); //2,肯定扩容阈值 threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); //3,初始化数组对象 table = new Entry[capacity]; initHashSeedAsNeeded(capacity); }
Q:如何确保获取到比toSize 最接近且大于等于它的2的幂次方的数?
深刻理解roundUpToPowerOf2方法:
private static int roundUpToPowerOf2(int number) { // assert number >= 0 : "number must be non-negative"; //若是number大于等于最大值 2^30,赋值为最大,主要是防止传参越界,number必定是否非负的 return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; //核心在于Integer.highestOneBit((number - 1) << 1) 此处 }
先抛出2个问题:
1:这个 (number - 1) << 1 的做用是什么?
2:这个方法highestOneBit确定是为了获取到知足条件的2的幂次方的数,背后的原理呢?
结论: Integer的方法highestOneBit(i) 这个方法是经过位运算,获取到i的二进制位最左边(最高位)的1,其他位都抹去,置为0,即获取的是小于等于i的2的幂次方的数.
若是直接传入number,那么获取到的是2的幂次方的数,可是该数必定小于等于number,但这不是咱们的目的;
如highestOneBit(15)=8highestOneBit(21)=16而咱们是想要获取一个刚刚大于等于number的2次方的数,(number-1)<<1 所以须要先将number 扩大二倍number <<1 , 为何须要number-1,是考虑到临界值问题,刚好number自己就是2的幂次方,如 number=16,扩大2倍后为32, highestOneBit方法计算后结果仍是32,这不符合需求。
public static int highestOneBit(int i) { // HD, Figure 3-1 i |= (i >> 1); i |= (i >> 2); i |= (i >> 4); i |= (i >> 8); i |= (i >> 16); return i - (i >>> 1); }
2的幂次方二进值特色:只有最高位为1,其余位全为0
目的:将传入i的二进制最左边的1保留,其他低位的1全变为0
原理:某数二进制: 0001 ,不关心其低位是什么,以*代替,进行运算
i |= (i >> 1); 0001**** | 00001*** ---------- 00011*** #保证左边2位是1
i |= (i >> 2); 00011*** | 0000011* ---------- 0001111* #保证左边4位是1
i |= (i >> 4); 0001111* | 00000001 ---------- 00011111 #把高位如下全部位变为1了,该数仍是只有5位,该计算可将8位下全部的置为1
Q:为何要再执行右移8位,16位?
因int类型 4个字节,32位,这样能够必定能够保证将低位全置为1;
i - (i >>> 1); #此时 i= 00011111 00011111 - 00001111 #无符号右移1位 --------- 00010000 #拿到值
分析2: HashMap如何处理key 为null状况,value呢?
结论:
private V putForNullKey(V value) { //1.直接table[0] 位置获取,先遍历链表(这里对该数组位置统称为链表,可能没有元素,或者只有一个元素,或者链表)查找是否存在相同的key,存在覆盖原值 for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; //此时注意添加节点时,第一个0即表明数组下标位置,后面会分析该方法 addEntry(0, null, value, 0); return null; }
分析3:如何实现hash算法,保证key的hash值均匀分散,减小hash冲突?
jdk1.7中为了尽量的对key的hash后均匀分散,扰动函数实现采用了 5次异或+4次位移
final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } //k的hashCode值 与hashSeed 异或 h ^= k.hashCode(); // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
分析4:插入新的节点到map中,若是原数组总元素个数超过阈值,先扩容再插入节点
void addEntry(int hash, K key, V value, int bucketIndex) { //总元素个数大于等于阈值 且 当前数组下标已存在元素了: 扩容 if ((size >= threshold) && (null != table[bucketIndex])) { //1,扩容,上面已分析过代码 resize(2 * table.length); //2,计算新加key的hash值,key为null的hash值为0 hash = (null != key) ? hash(key) : 0; //3,确保计算的数组下标必定在数组有效索引内,见分析5 bucketIndex = indexFor(hash, table.length); } // 4,扩容后再插入新数组中 createEntry(hash, key, value, bucketIndex); } //分析5 static int indexFor(int h, int length) { // 与数组长度-1与运算,必定能够确保结果值在数组有效索引内,且均匀分散 return h & (length-1); } // 进一步分析插入节点方法 void createEntry(int hash, K key, V value, int bucketIndex) { //1,首先获取新数组索引位置元素 Entry<K,V> e = table[bucketIndex]; //2,头插法插入新节点, Entry构造方法第4个参数e表示指定当前新增节点的next指针指向该节点,造成链表 table[bucketIndex] = new Entry<>(hash, key, value, e); //3,map元素个数+1 size++; }
参考: