HashMap1.7 问题总结

  Java SE 源码在面试中的考查也算是重中之重了,最近看了HashMap1.7 的源码,对其中一些代码的设计以及线程不安全等引发的问题,在此记录随笔,如有不正之处望指出。

 为什么hashMap的负载因子是0.75

   hashMap 的负载因子为0.75是一个常识性问题,但是为什么负载因子不为0.5、0.6或者是1和0呢?
查看API 源码:
在HashMap注释中有这么一段:

Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million

  

翻译过来大概的意思就是:

容器中的节点遵循泊松分布,默认调整大小的参数平均约为0.5,阈值为0.75,虽然由于方差较大调整粒度。忽略方差列表大小k的出现次数是(exp(-0.5) * pow(0.5, k) /
阶乘(k))。(——有道)

可见当负载因子为0.75 的时候,桶中同一节点上链表超过8个元素的情况几乎不可能发生。负载因子的作用在于判断map 是否需要扩容,而扩容是一个比较耗时的操作,因为它需要遍历赋值。

如果负载因子过小,比如为0.5。在初始容量为16 的情况下,超过8个就会扩容到32,而未使用的空间为24,利用率仅为1/4。频繁的扩容不仅耗时而且浪费空间。

如果负载因子过大,比如为1 。桶中的元素达到16 过后才会扩容,虽然空间利用率大,但是桶中元素的冲突肯定变多。而map 数据结构的好处就在于理想情况下获取元素的时间复杂度为O(1),负载增大会影响map 的查询等操作。

简而言之HashMap负载因子为0.75是空间和时间成本的一种折中。


为什么HashMap 的初始容量为2 的N 次幂

  关于为什么HashMap 的初始容量为2 的N 次幂这个问题,网上的答案有很多,但是大多数都只是答道了:再次hash 的值 & (length - 1) 就相当于取余,保证下标在容量之下。但是这样的回答,我个人感觉有点牛头不对马嘴(没有嘲讽的意思,只是感觉怪怪的)。因为 & 操作肯定小于或等于最小的那个数。
  哈希的意义在于使分布平均,前面两次哈希就可以看出这个思想。理所因当,我们在取下标的时候也应该如此。后来我最算是看到了比较合理的解释:

之所以取 2 的 N 次幂的原因是,(length - 1) ---> 它的二进制位都为1
而 1 & (0 /1) 的结果也是(0 / 1),如果是 0 那结果一定为0
如果一定为0 就意味着一些下标值是取不到的,不够平均

比如我们取容量为 15


public class Test {
int hash(Object k) {
int h = 0;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}

h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

public static void main(String[] args) {
for (int i = 1 ; i <= 12 ; i++){
System.out.println((new Test().hash(new Test())) & 14);
}
}
}
输出:
4
14
2
14
8
14
2
4
14
12
4
0
Process finished with exit code
0

输出的结果可以看出取值都为偶数,因为14 的最后一个二进制位为0 。

讲一讲HashMap 的put 方法引发的问题

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        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;
            }
        }

void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

//添加节点
void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

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];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

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;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

  

  在讲put 引发的问题之前我们先总结一下put 函数做了哪些事情

  •  判断map 有没有初始化
  • 判断插入的key 是不是为null ,如果是插入null 的位置(table[0])
  • 取得下标,遍历链表判断有没相同的key ,有则添加新值返回旧值
  • 没有则添加元素
  • 添加元素先判断需不需要扩容,扩容采用头插法
  • 创建新节点添加对应位置

HashMap 并不是线程安全的类,所以在resize() 操作时,并发下可能会造成的问题

  • 形成环形链表

并发插入:

  • 丢失值

就不说明原因了,网上多。

原文地址:https://www.cnblogs.com/tanwt/p/9829647.html