HashMap底层原理分析
学习hashmap之前先了解一下arraylist
arrayList数据结构:查询效率高,增删效率高
ArrayList<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
for(int index=0;index<list.size();index++) {
list.remove(index);
System.out.println("remove");
}
System.out.println(list.size());
打印结果:(只删除三个元素)
remove
remove
remove
2
底层解释:
for循环第一次的时候,数组下标0,1,2,3,4,此时remove的正好是“a”,size=5;index=0
a | b | c | d |
---|
第一次结束之后数字会copy一遍,进行压缩,第二次数组下标是0,1,2,3,此时的size=4,index=1,此时remove的正好是“c”
b | c | d | e |
---|
同理copy数组,进行压缩,第三次数组下标是0,1,2,此时size=3,index=2,remove的正好是“e”
b | d | e |
---|
同理copy数组,进行压缩,第四次数组下标是0,1,此时size=2,index=3,没有满足条件的remove数据,跳出循环,最终剩下了“b”,“d”,也就解释了上边删除了remove三次,size=2了
b | d |
---|
linkelist数据结构,增删效率高,查询效率低
hashmap数据结构:
是一个综合的由:数组,链表,红黑树组成
hashmap的初始table(数组)长度是1<<4(1<<4,1左移四位得10000=2四次方=16),源码:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
最大长度:
static final int MAXIMUM_CAPACITY = 1 << 30;
当map.put()多少数据时需要自动扩容,根据源码里的加载因子判断,即:
static final float DEFAULT_LOAD_FACTOR = 0.75f;//百分之七十五,四分之三
比如我们的初始值是16,则在我们put到160.75=12的时候,数组就会自动扩容162=32,以此类推
当链表节点足够长,会有个链表向红黑树转化的过程(为什么是8,根据查找的时间复杂度有关,大于8个节点时,红黑树的查找效率比链表的高)转化因子是:
static final int TREEIFY_THRESHOLD = 8;
同理,如果节点小于这个转化因子,会有个红黑树想链表转化的过程,转化因子是:
static final int UNTREEIFY_THRESHOLD = 6;
hashmap的算法
数组下标的计算方法:(n-1)&hash值,n代表一个hashmap里的数组长度,默认是16
hash值不同,但是(n-1)&hash的结果可能会是相等的,这就有个可能同一个位置下有一个链表形式的存数方式,如上图的X0位置
hash函数源码:经过hash函数运算得到key值与上(n-1)得到数组下标(index)值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可以这么写:
static final int hash(Object key) {
if(key==null) return 0; //key的空值处理
int h = key.hashcode();//32位,任何一个对象都会有一个hashcode()方法,public native int hashCode();返回值是int,所以是一个32位的
int temp = h>>>16;//向右位移16位,目的是为了使高位参与运算
int newHash = h^temp;//异或运算
reture newHash;//得到hash值
}
操作 原理
增加 1.先计算key的hash值2.根据hash值找到数组位置3.如果数组为空则插入,否则往链表的尾部添加元素
查找 1.先计算key的hash值2.根据hash值找到数组位置3.如果找到则返回,否则遍历链表找到对应的节点
删除 1.先计算key的hash值2.根据hash值找到数组位置3.如果找到则删除,否则从链表中找到对应节点,然后删除节点
put方法核心流程(JDK1.8之后的尾插法),源码解析
1.计算hash值找到数组位置
2.对应位置数组位空则插入
3.否则往链表的尾部添加元素
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; //定义一个数组tab
Node<K,V> p; //p-->上图中5位置第一次进来的那个值即node节点
int n; //数组长度
int i; //数组下标index
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; //这两步,如果数组是空的就会数组初始化,得到数组长度n
if ((p = tab[i = (n - 1) & hash]) == null) //i=(n - 1) & hash计算数组下标的位置,这个位置如果为null
tab[i] = newNode(hash, key, value, null); //相当于直接插入到list里,且next=null
//Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
// return new Node<>(hash, key, value, next);
//}
else { //else如图5的位置已经有”king“这个值了,就要在这个位置,就要往下链表形式存数据
Node<K,V> e; K k;
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))//判断key值是否相等
e = p; //如果再次插入的key值跟之前的一样,把old节点p负值给e,直接跳到下边的if语句
else if (p instanceof TreeNode)//链表转成红黑树的过程,树化的过程
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//树化的过程
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//判断上图中的king的next==null,
p.next = newNode(hash, key, value, null);//则把peter插入到king的后边
if (binCount >= TREEIFY_THRESHOLD - 1) // 判断如果链表过长,会树化过程
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) //同理如果key相同,会值覆盖,并返回oldvalue
break;
p = e;//此时e=p.nextNode,否则一直这个for循环,找到next==null,把值put进去
}
}
if (e != null) { // existing mapping for key,跳到这里
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;//新值覆盖旧值,然后返回oldvalue
}
}
++modCount;
if (++size > threshold) //threshold就是加载因子,如果size>threshold,则会把tab扩容
resize();
afterNodeInsertion(evict);
return null;
}
put方法中注意点
1.相同的key,会返回一个oldValue
2.链表过长,会进行一个树化过程
3.size过长,会根据加载因子计算什么时候扩容
4.hashmap为什么要扩容?
同一个下标位置下的链表过长,会影响查询效率
5.怎么扩容
原size*2
6.hashmap线程不安全
7.ConcurrenHashMap如何解决线程安全问题
CAS机制(cup会一直跑),synchronized锁
8.ConcurrentHashMap在JDK1.8中为什么要使用内置锁synchronized来代重入锁ReentrantLock
synchronized:多种状态
单线程(偏向锁标志1–哪个线程)
多线程交替执行(取消偏向锁)-----(升级)---->使用CAS锁(轻量级锁)-----(升级)----->重量级锁(没有抢到 锁,就阻塞)
CAS—>CPU响应时间很快,吞吐量不高
重量级锁,上下文切换,吞吐量比较高