HashMap 底层
在JDK1.8 之前是数组 + 链表,但在JDK1.8 之后变成了数组 + 链表 + 红黑树。当hash表中存储的键值对超过了数组长度乘以负载因子(0.75)时则会进行hash扩容变成原来的两倍,且当链表的长度超过一个阈值(8)的时候则会将链表转为红黑树其时间复杂度时Olog(n)。
HashMap为什么要使用红黑树而不是AVL树
其原因时红黑树和AVL树的查询都是采用二分查找查询效率都比较高,但由于AVL追求绝对的平衡这样在插入和删除的时候会非常耗时,而红黑树其允许局部不平衡,所以在插入和删除的时候其效率高于AVL树
hashMap 扩容会导致死循环?
在Jdk1.8之前,hashMap采用头插法进行扩容,在多线程的情况下如果线程A、B同时对其进行扩容的话因为其采用头差法的方式进行扩容其原因是采用头差法的时会改变原数组entey的指向关系,即a->b->c,采用头差法则会变为c->b->a。
1、 在 A线程进行扩容的时候 获取到 节点a且知道其下一个节点是b了
2、这时被B线程抢去执行权且B线程完成了扩容,这时数据变为 c->b->a
3、此时A线程接着运行则会变为 b -> a,这时由于entey是共享的由于 元数据被改为 c->b->a此时发现b指向的是a了,则A线程执行完之后的数据会变为 a -> b -> a,此时就造成循环了
在Jdk.8之后采用尾插法就避免了这一问题
hash算法优化
hash算法优化:对每个hash值的让其高低16位进行异或操作,让其同时保留了高低16为的特性。避免hash冲突。
寻址算法的优化:用与运算替代取模运算。
static final int hash(Object key) {
int h;
// 先获取到hash值,然后异或hash值的右移16为,在与原值进行异或运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 寻址优化
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
哈希寻址优化
用hash & (n - 1)进行与运算,其和hash值对 n 取模效果一样。只是其性能要好。
哈希冲突解决方法
使用数组+链表的方式解决哈希冲突问题。当链表长度超过8的时候就转红黑树
哈希扩容
当数组长度满了的时候,采用两倍扩容的方式进行扩容。然后在做 再 hash。
ConcurrentHashMap
- JDK 1.8之前ConcurrentHashMap采用分段加锁。
[数组1],[数组2],[数组3],[数组4] ----->每个数组的下标都对应一个锁。
- JDK 1.8之后做了锁粒度优化采用进行CAS+synchronized 进行加锁
[一个大数组],数组里每一个元素进行put操作时都有一个不同的锁,如果两个线程同时操作数组[1]的位置进行put操作,则这时会对数组[1]这个位置进行put的时候会采用CAS的策略。
同一时间只有一个线程能够执行成功这个CAS(Compare And Swage),其它都会执行失败。此时如果put失败则会采用synchorized(数组[1])进行加锁。