hashmap 基本特性
java 1.7 用的是数组 + 链表
java 1.8及以后 是数组 + 链表 + 红黑树
hashmap数组大小默认是16
hashmap的get,put 操作的时间赋值度是O(1)
put的时候,使用 key.hashCode()/16 作为数组的下标
hash碰撞
值不一样但是他们的cashcode有可能一样,或者取余16后余数可能一样,就需要往一个数组下标下放入多个值,
这个时候需要使用链表来存储这个值,采用头插法。链表时间复杂度O(n)
实际上hashmap里面使用的是位运算达到取余的目的,效率更高。
loadfactor默认的加载因子75%,为什么是0.75 ?取的是空间和时间上比较均衡的值。
capacity必须是2的指数次幂,如果你传入的值不是,就会比你的值大的2的指数次幂的值。这样做的目的指数为了进行取余运输的效率更高,具体做法是,比如你当前值得hashcode 是 1011 0111 1001 1100
2的指数次幂-1, 比如是2的5次幂-1,即二进制值为 0000 0000 0001 1111
使用2指数次幂-1与hashcode进行与操作,如下,刚好达到取余的目的。
1011 0111 1000 1100
0000 0000 0000 1111
--------------------------------
0000 0000 0000 1100
hashmap扩容:
扩容阈值 threshold = capacity * 加载因子(0.75)
对于一个数组长度是16的hashmap如果存到16*0.75 = 12个元素时候,就会考虑进行扩容了,扩容即容量增加一倍,变成2的n+1次幂
扩容的时候,单线程会让链表的先后颠倒。
多线程扩容的时候有可能导致链表成环,会导致下次执行put的时候发生死锁。
java 8 后, 当链表过长,TREEIFY_TREE > 8 并 capacity > 64 的时候进行进行转成红黑树。如果小于64有限扩容。
1.8 在大数据量的时候比1.7的性能有所提升。
为什么TREEIFY_TREE 被设定为8,因为根据泊松分布统计,链表长度为8的的概率为6/千万,这样的的概率适中。
java 8 后,如果解决扩容死锁问题:
如下代码,e.hash & oldCap 通过old capacity 于当前的hash值进行与运算,来判断链表中的节点是继续留在当前位置还是需要被移动到新高位数组中。
原理:
如果old capacity继续是16的情况下。
1011 0111 1000 1100
0000 0000 0001 0000
--------------------------------
0000 0000 0001 0000
对于倒数第5位的hashcode 可能是1或者0,如果是1,说明如果用扩容后的2的n次方-1的值进行与hashcode的时候就会得到一个比16更大的数,如果是0,所以该数的hashcode与扩容后的2的n次方-1的值进行与的时候得到的值还是原来的值,所以,用这种巧妙的方法代替了java1.7中的头插发。
1011 0111 1000 1100
0000 0000 0001 1111
--------------------------------
0000 0000 0000 1100
{ // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
对应于自己的分库分表进行扩容可以使用类似巧妙的方法。
java 1.7,ConcurrentHashMap 分段加锁,把数据分别存在不同的segment下面,每次put操作只对当前segment加锁,不同的segment下面的操作相互不影响。多个线程需要操作同一个segment下面的的数据时需要先拿到锁先。segemnt里面不放数据,数据都放在segmeng下面的小的hash表里面。
基本原理就是大表套小表。
java 1.8,ConcurrentHashMap 锁加在了每个linklist/tree的第一个元素上面,比1.7的segment上更细。因为每个segment下面一个table,table的每个槽位有一个linklist/tree.
hashtable 对整个数组加锁,一个hashtable就一个锁,所以的线程操作一个锁,效率更低。
ArrayList 线程不安全
要使用线程安全的可以用:
Collections.synchronizedList 读写都加锁,效率低
CopyOnWriteArrayList 读写分离,空间换时间,只能保证最终一致性。
跳表-在linklist的基础上再维护一个索引空间换时间,时间复杂度O(Log(n))