增大负载因子可以减少Hash表(就是那个Entry[]数组)所占用内存空间,但会增大查询数据的时间开销,而查询是最频繁的操作(HashMap的get()和put()都要查询)
减少负载因子可以会提高数据查询的性能,但会增加Hash表所占用的内存空间。
现在我们合理的调整负载因子的值了。如果程序比较关心内存的开销,适当增加负载因子。如果比较关心时间开销,则适当减小负载因子,其实大部分情况下保持负载因子默认的0.75即可。
多线程环境下,使用HashMap进行put操作引起的问题分析
在多线程的环境下,多个线程对HashMap数据进行put操作时会导致死循环。而造成这个原因的罪魁祸首就是因为HashMap的自动扩容机制。
我们来观察HashMap的put过程
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } //当key为null,调用putForNullKey来处理 if (key == null) return putForNullKey(value); //根据key的hashCode计算hash值 int hash = hash(key); //搜索制定hash值在对于table中的索引 int i = indexFor(hash, table.length); //如果i处索引不为null。则通过循环不断遍历e元素的下一个元素 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; //找到指定key与需要放入key相等(即两者hash值相同,equals返回true) if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } //如果i索引处的entry为null。表明此次没有entry。直接插入在此次即可 modCount++; //添加key,value到索引i处 addEntry(hash, key, value, i); return null; }现在我们可以看出:当程序试图将一个key-value对放入HashMap中时,首先根据key的hashCode()返回值决定该Entry的存储位置,如果两个key的hash值相同,那么它们的存储位置相同。如果这个两个key的equalus比较返回true。那么新添加的Entry的value会覆盖原来的Entry的value,key不会覆盖。如果这两个equals返回false,那么这两个Entry会形成一个Entry链,并且新添加的Entry位于Entry链的头部。
观察addEntry(hash, key, value, i);源码:
void addEntry(int hash, K key, V value, int bucketIndex) { //获取指定bucketIndex处的Entry Entry<K,V> e = table[bucketIndex]; //将新创建的Entry放入bucketIndex索引处,并让新Entry指向原来的Entry table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //如果Map中的key-value对的数量超过了极限 if(size++>=threshold){ //扩充table对象的长度到2倍 resize(2*table.length); } }现在我们看罪魁祸首的resize方法的源码:
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; ...... //创建一个新的Hash Table Entry[] newTable = new Entry[newCapacity]; //复制旧数据到新数组中: transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); }复制旧数据到新数组中源码:
void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; if (e != null) { src[j] = null; do { Entry<K,V> next = e.next; int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }画了个图做了个演示。
我假设了我们的hash算法就是简单的用key mod 一下表的大小(也就是数组的长度)。
最上面的是old hash 表,其中的Hash表的size=2, 所以key = 3, 7, 5,在mod 2以后都冲突在table[1]这里了。
接下来的三个步骤是Hash表 resize成4,然后所有的<key,value> 重新rehash的过程
多线程下的rehash:
1)假设我们有两个线程。我用红色和浅蓝色标注了一下。