hashmap

1.为什么用(h = key.hashCode()) ^ (h >>> 16)算hash?
这要从数组下标位置的确定来考虑:tab[i = (n - 1) & hash],数组下标的确定是数组长度-1然后&元素的hash值。
1.1(n - 1) & hash为什么能保证数组下标不能越界呢?
这就跟hashMap容量有关,hashMap巧妙的利用了2的幂方来作为容量,默认情况下初始容量为16,之后扩容是旧容量的2倍。我们以16为例:16-1 =15,写成二进制:1111,这种二进制跟任何一个二进制取&都会小于15,这就相当于%操作,但&的效率会高很多。下标现在是可以确定了,但如果元素的hash值不够散列,就会造成hash碰撞问题,hash值相同的key会以单链表或红黑树的形式储存。Hash碰撞比较严重的话会严重影响hashMap查询和插入的性能,所以应该尽量使hash值随机。
从上面我们知道一个容量小于2的16次方的hashMap,元素的高16位是完全没有用的,顶多只能用到低16位,因为2的16次方-1写成二进制:0000 0000 0000 0000 1111 1111 1111 1111,高16都是0,跟任何数&高16位都会是0.所以在这种情况下高16位几乎没有用,不会影响元素的位置,而大多数情况下hashMap的元素都不会超过2的16次方.所以应该要想办法让元素hash值高16位参与下标确定.
h >>> 16就是向右移16的意思,此时hash的高16就变成了低16,现在就是高16怎样跟低16运算才能足够的随机,因为结果越随机,(n - 1) & hash算出来的值就会更随机.&和|只有0和1结果,^会更加均匀.(h = key.hashCode()) ^ (h >>> 16)算hash的方式就出来了.
2.长度为什么是的2的幂次方
2.1可以使用(n - 1) & hash方式确定下标,效率更高。
2.2扩容时重新计算位置会更加简单(针对有hash冲突情况),扩容后新容量是旧容量的2倍,相当于原二进制向左移动一位,比如之前容量是16,二进制是1 0000,扩容后是32,二进制是
10 0000.也就是元素的高位部分最后一个会参与(n - 1) & hash位置计算,而使用hash&oldCap方式就能确定参与运算的高位会不会改变原来的位置.当参与计算的高位是1时,新位置=j+oldCap,当参与计算的高位是0时,新位置=j。
3.1.8如何解决了1.7之前会造成死循环的问题?
3.11.7会造成死循环的由来
假设现在有a和b两个元素的key的hash值冲突了,那么这两个元素就会以链表的形式储存:a->b->null.现在线程1和线程2都来进行扩容,假设线程1和线程2同时执行Entry next = e.next; 然后线程1暂时挂起,线程2继续执行扩容,扩容后:b->a->null.此时线程1开始执行执行Entry next = e.next;,a的next是b,执行之后:a->null,此时在执行执行Entry next = e.next;b的next是a,执行之后b->a->null,再执行执行Entry next = e.next;a的next是null。执行之后:a->b->a->null.这就形成了环状,在get时就会形成死循环。
3.2 1.8如何解决死循环
从上面可以看出,形成环的主要原因是要把单链表倒置,也就是头插入,如果是尾插入就不会改变next指针的指向。也就不会形成环状链表,所以1.8使用了尾插入,而且使用尾插入还有一个好处是hash冲突元素扩容新位置会很好计算,上面以阐释,这里就不展开说明了。
3.21.8 还有什么线程安全问题?
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null)
可能会形成数据覆盖,线程a和线程b同时执行(p = tab[i = (n - 1) & hash]) == null)且i是一样的,之后线程a挂起,线程b执行,之后线程a再执行就会覆盖线程b的元素。
++size
可能使size跟实际size不一样。

你可能感兴趣的:(java相关)