了解,比如通过名称指定配置信息,建立对象与对象的映射关系,设置缓存从而可以避免频繁查询数据库和文件系统,提高效率,使用hashmap存储请求路由规则,根据路由规则以及请求参数查找对应的服务器列表。
JDK >= 1.8的时候是数组+链表+红黑树,Hash值产生碰撞时,链表长度>8会由链表转换成红黑树(将链表转换成红黑树前判断,如果当前数组长度小于64,那么选择先数组扩容,而不是转换成红黑树),当红黑树节点<6的时候,会由红黑树转换成链表,就是二者的性能临界点。
HashMap的默认初始化大小为16,之后每次扩充容量变为原来的2倍,并且HashMap总是使用2的幂作为哈希表的大小。
HashMap底层使用红黑树的主要原因是为了在桶中存在大量键值对时提高查找效率。当桶中的链表长度较长时,查找效率可能会变得比较低,这时可以将链表转换为红黑树,以便在O(log n)的时间复杂度内查找键值对。
平衡二叉树的初衷是解决普通二叉树在频繁的插入,删除等动态更新的情况下出现的时间复杂度退化的问题。
红黑树与avl树对比:
红黑树是近似平衡的。相比于avl树,检索时效率差不多,都是通过平衡来二分查找,但是对于插入删除等操作提高很多。红黑树不像avl树一样追求绝对平衡,它允许局部很少的不完全平衡,这样对于效率影响不大,但省去了很多没有必要的调平衡操作,avl树调平衡有时候代价很大,所以效率不如红黑树。
红黑树高度只比高度平衡的AVL树的高度(log2 n)仅仅大一倍,性能上却好很多。
AVL树是一种高度平衡的二叉树,所以查找的非常高,但是有利就有弊,为了维持这种高度平衡,要付出更多代价,每次插入删除都要调整,比较复杂耗时。对于有频繁的插入和删除操作的数据集合,使用AVL树的代价就有点高了。红黑树的插入,删除,查找各种操作性能都比较稳定。
防止链表过长,从而导致查询效率过低,红黑树有自平衡的特点,可以防止不平衡的情况发生,时间复杂度由n变为lgn。
更多还是为了防止用户自己实现了不好的哈希算法时导致链表过长,从而导致查询效率低,转化为红黑树更多是一种保底政策。
红黑树的个数被删为6的时候会回退回链表。
HashMap提供了put方法添加元素,putval方法只是给put方法调用的一个方法,如果数组为空或者长度为0就扩容,不为空的话,如果定位到的数组位置有元素就和要插入的key比较,如果key相同就直接覆盖,如果key不同就判断p是否是一个树节点是否是一个树节点,如果是就调用e = ((TreeNode
将元素添加进去,如果不是就遍历链表插入(插入链表的尾部),当链表长度大于阈值并且数组长度超过64就会执行链表转红黑树的操作,否则就只是对数组扩容。
jdk8在扩容时计算数组元素下标时做了优化:
jdk1.7:
重新计算每个元素的哈希值
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
jdk1.8:
在扩容时没有像JDK1.7那样,而是通过高位运算e.hash & oldCap
,来确定元素是否需要移动。
do {
next = e.next;
// resize前后,桶的位置未变化
// 尾插法插入链表
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// resize后新产生的桶
// 尾插法插入链表
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//不同移动
key1.hash = 10 0000 1010
oldCap = 16 0001 0000
//需要移动
key2.hash = 10 0001 0001
oldCap = 16 0001 0000
不安全,如果两个线程同时对一个HashMap操作,一个线程在其中添加或删除元素,另一个正在遍历该HashMap,这种情况可能导致一个线程看到不一致的状态。
import java.util.HashMap;
public class SynchronizedHashMapExample {
public static void main(String[] args) throws InterruptedException {
HashMap<String, Integer> map = new HashMap<>();
// 添加元素
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
// 创建锁对象
Object lock = new Object();
// 第一个线程遍历HashMap
Thread thread1 = new Thread(() -> {
synchronized (lock) {
for (String key : map.keySet()) {
System.out.println(key + ": " + map.get(key));
}
}
});
// 第二个线程在遍历过程中删除一个元素
Thread thread2 = new Thread(() -> {
synchronized (lock) {
map.remove("B");
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
jdk7死循环例子:
5张图讲明白JDK1.7下的HashMap死循环(原理+实战) - 知乎 (zhihu.com)
关键点:线程2先扩容完毕,此时布局是这样的,但是线程1却还在按照只有两个桶的时候的布局来进行扩容,读取数据的时候却又是按照T2已经扩容完毕的数据来进行。
可以使用同步机制:可以使用Java中的synchronized关键字来保证多个线程对HashMap的同步访问。在使用synchronized关键字时,需要确保所有对HashMap的访问都在同步代码块中完成,以保证线程安全。
考虑到热点数据的原因,即最近插入的元素也很可能最近会被使用到,所以为了缩短链表查找元素的事件,所以每次都插入到表头,并且头插法插入速度最快,找到数组位置就直接找到插入位置,但是可能会造成多线程同时扩容出现死循环。
0.75是为了触发扩容,减少冲突发生的概率,加载因子很大,扩容条件就会苛刻,hash碰撞概率变高,每个链表长度都很长,查询速度变慢,太小又会导致扩容频率变高,,内存消耗变大。
需要,否则就需要多次扩容,取2的幂次方,也就是128即可,因为2的幂次高位是1其余位置是0,当进行hash运算时,(n-1)&hash,n-1就是高位是0,低位都是1,这样&完之后,结果各个位置的取值取决于hash,如果不是2的整数次幂必然会有0位,0与任何&肯定为0,会造成更多的哈希冲突。
回答指导:上面这些问题,不应该需要记住!你需要理解,先理解大概=》看一下源码怎么做的,反正回答出上面这些问题,基本稳