在学习juc并发集合前先看了下HashMap,听说好多面试会问这个,没遇见过,学习下吧。学习的jdk源码一直都是1.7版本的,其他版本可能有些微不同,应该也不影响学习。
HashMap有几点需要记得吧:
1.是非线程安全的:javadoc说明:可以通过Map m = Collections.synchronizedMap(new HashMap(...));解决,或者干脆用juc里面ConcurrentHashMap;
2.内部存储使用数组加链表的结构;
3.容许使用null作为key和value。
HashMap使用数组加链表的结构解决hash冲突,大致结构为:
元素添加时计算出一个hash,然后算出在数组中位置,hash值相同的值再用链表来关联。
基本的变量:
<span style="font-size:18px;">/**如果构造没传入初始化数组大小,这个就是默认数组大小 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** 最大容量*/ static final int MAXIMUM_CAPACITY = 1 << 30; /** 构造的时候如果没有传入负载因子,就使用这个默认的,主要是控制整个数组的使用 假如构造的时候直接HashMap(),那么初始的时候数组大小就是16,这个0.75,所以整个数组你只能用12,然后你再put的时候就会扩容,新的大小大概是数组大小*2 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** 就是初始put,数组没有扩容的时候用来判断下 */ static final Entry<?,?>[] EMPTY_TABLE = {}; /** 这个就是HashMap内部的结构,一个Entry数组,大小最好为2的倍数 */ transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; /** HashMap元素个数 */ transient int size; /** 这个就是用来保存上面数组大小*负载因子的值,存储的极限值。数组为空的时候为初始容量大小,每次扩容的时候需要重新计算 */ int threshold; /** 负载因子 */ final float loadFactor; /** 修改次数,用于迭代时候检查HashMap有没有被增删 */ transient int modCount; /** 系统启动后,获取配置的系统参数,计算hashseed时候用到 */ static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE; 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; } } /** 计算hashcode的使用随机数,初始化hashseed的时候,会看是否需要重新获取这个值,一般也不会 */ transient int hashSeed = 0; </span>
<span style="font-size:18px;">static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; //链表结构,指向下一个Entry int hash; //计算出来的hashcode值 /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } public final K getKey() { return key; } public final V getValue() { return value; } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry)o; Object k1 = getKey(); Object k2 = e.getKey(); //判断实例是否相等或实例的key和value是否一样 if (k1 == k2 || (k1 != null && k1.equals(k2))) { Object v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } public final int hashCode() { return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); } public final String toString() { return getKey() + "=" + getValue(); } /** key值存在,再put的话调用LinkedHashMap用了 */ void recordAccess(HashMap<K,V> m) { } /** * This method is invoked whenever the entry is * removed from the table. */ void recordRemoval(HashMap<K,V> m) { } }</span>
<span style="font-size:18px;">public HashMap(int initialCapacity, float loadFactor) //构造传入了初始容量和负载因子,下面校验2个值 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; threshold = initialCapacity; //极限值初始化为容量大小,后面put的时候才计算 init(); //空方法,子类实现 } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } /** 上面3个构造都没有初始化数组,留到了后面put的时候,这个根据其他map构造,就在初始化后直接扩容数组 */ public HashMap(Map<? extends K, ? extends V> m) { this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); inflateTable(threshold); //数组扩容 putAllForCreate(m); }</span>
put:
<span style="font-size:18px;">public V put(K key, V value) //table初始的时候是Empty的,在这里才会真正创建 if (table == EMPTY_TABLE) { inflateTable(threshold); //扩容数组 } if (key == null) //支持key为null的情况,放在table[0] return putForNullKey(value); int hash = hash(key); //key不为null,计算hashcode int i = indexFor(hash, table.length); //查找在table中位置 //for轮询指定位置的所有节点,检查新节点的key值是否存在,存在就替换value 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))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); //如果key不存在就加入到指定位置的第一个位置 return null; } //将key为null的放在table[0] private V putForNullKey(V value) { //for循环判断链表中是否存在相同key的Entry,如果有更新value,后面对于key不为null的Entry处理时也有这一步 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; } } //如果没有找到key相同的就要添加到链表里面 modCount++;//table的结构有编号modeCount要自增 addEntry(0, null, value, 0); //添加到链表里面 return null; } //添加到链表里面 void addEntry(int hash, K key, V value, int bucketIndex) //如果节点数量大于极限值并且位置已经使用了,需要重新初始化table,并将数据转移 if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); //需要重新扩容table,并将原有的数据移到新的里面 hash = (null != key) ? hash(key) : 0; //重新计算hash bucketIndex = indexFor(hash, table.length); //重新计算hash在table中的存储位置 } createEntry(hash, key, value, bucketIndex); } //重构table,容量*2 void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; //新建table transfer(newTable, initHashSeedAsNeeded(newCapacity)); //将数据迁移 table = newTable; //将table指向新创建的table threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); //重新计算极限值 } //将原table数据迁移到新创建的table,高并发的时候有问题 //假如原table[1]是e1->e2->e3 //假如运气好都迁移到新table的table[3]位置,迁移后的节点顺序为e3->e2->e1,后面画图说明 void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; //循环数组 for (Entry<K,V> e : table) { //循环链表 while(null != e) { Entry<K,V> next = e.next; //原table的next if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); //重新计算在新table中位置 e.next = newTable[i]; //将原table的节点的next指向新table指定位置的节点 newTable[i] = e; //将新table指定位置的节点换成e e = next; //e指向原table的next } } } //计算key的hash值,说实话,没看懂hash算法,期待大神 final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } 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); } //计算key值hashcode在table数组中的位置 static int indexFor(int h, int length) { //这里的length就是table里数组的长度,最好为2的倍数,使用&实现数组轮询, //其实如果length为奇数的话,可以用h%length实现数组轮询,只不过%有除法运算,肯定有性能影响(之前看netty4,里面group.next()时候就2种都支持),所以这里使用& return h & (length-1); } //创建新节点,并添加到table指定位置 void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; //创建新的节点时,将原有位置的节点传入,这样新建节点的next指向原有节点,新添加的节点放到table指定位置的第一个 table[bucketIndex] = new Entry<>(hash, key, value, e); size++; //size自增 } /** 扩容table */ private void inflateTable(int toSize) { // 计算>=toSize的2的倍数 int capacity = roundUpToPowerOf2(toSize); threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//重新计算极限值 table = new Entry[capacity]; initHashSeedAsNeeded(capacity); //初始化hashseed } //这里就是就算>=number的2的倍数 private static int roundUpToPowerOf2(int number) { //比较是否超过最大容量,highestOneBit返回二进制数的左边高位1的位置,如1100,返回的则是1000 //因此,如果你构造传入的2的倍数的话,减1后左移以为还是自己; //奇数的话就是,如15,就变成15->1101->1100->11000->10000->16,最后table的容量就是16 return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; } //初始化hashseed,一般hashseed是0,如果扩容的时候配置系统参数比容量小,就会randomHashSeed计算 final boolean initHashSeedAsNeeded(int capacity) { boolean currentAltHashing = hashSeed != 0; //hashSeed默认是0,这个false boolean useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); //这个在jvm启动后获取系统参数跟table容量比较 boolean switching = currentAltHashing ^ useAltHashing; if (switching) { hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this) : 0; //randomHashSeed计算不懂,百度了下,说在jdk7u40版本以下,高并发下,可能有性能影响,不过高并发也不会用HashMap了 } return switching; }</span>
get方法和transfer方法的具体等下次补充,休假先!
补充下get方法,get方法比较简单,get(key),hash=hash(key),然后在table里面查找index,找到index后循环对应位置的链表,找到相等的entry(通过hash和key判断相等),返回找到entry,返回entry的value:
<span style="font-size:18px;">public V get(Object key) { if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); } //table[0]获取key为null的entry private V getForNullKey() { if (size == 0) { return null; } for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; } //key不为null的情况获取entry final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); //也是循环查找 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null;<span style="font-family: Arial, Helvetica, sans-serif;">}</span></span>
分析下多线程情况下transfer方法导致链表形成环形结构,然后get方法获取时for循环挂死。
1.假如e1,e2两个entry hash后index相同,resize后也一样,正常结束后新的table结构为:
2.假如2个线程t1/t2,t1运行到transfer的Entry<K,V> next = e.next;线程挂起,t2运行结束,此时table为:
t2运行结束,t1继续运行第一个循环,变为:
一次循环;t1在运行前:e指向e1,next指向e2,第一遍while循环结束,此时t1的table[1]指向e1,循环中的e由原来的e1->e2,next则因为之前的因为t2之前修改的原因变为e1;
二次循环:因为e指向e2不为null,再来一次循环,结束时:
三次循环:二次循环后e又指向了e1,由于此时的e->next为null,所以第三次循环后不再处理,循环后e1->next指向e2,e2->next指向e1:
最后将table指向将新创建的table,我们在put或get操作时都会for循环查找链表,如果这个链表正好是这个循环链表,那就会引发死循环。
hashmap的引发cpu100%的原因解析:
http://coolshell.cn/articles/9606.html hashmap
http://my.oschina.net/xianggao/blog/393990?fromerr=RV7HfY4S
http://ifeve.com/hashmap-infinite-loop/