目录
一、HashMap
非线程安全,无序,jdk1.8之前,基于数组+单链表实现,jdk1.8之后,采用数组+链表+红黑树实现。
put方法逻辑:
1. 判断数组table是否为空或者长度length等于0,如果是,则调用resize方法初始化table数组
2. 通过key的hash函数值 table[i = (n-1)&hash] 计算索引位置。若该索引位置为空,则在该索引位 置新建一个节点作为头节点,并插入该
3. 若table数组该索引位置不为空,声明一个节点Node<K, V> e
3.1.判断是否等于该索引位置的第一个节点,若键的值和hash值都相等,把该节点赋值给节点e
3.2.若不等于该索引位置的第一个节点,再判断第一个节点是否为红黑树节点,若是红黑树节 点,调用红黑树的putTeeValue方法查找目标节点,并把该节点赋给节点e
3.3.若不等于该索引位置的第一个节点,也不是红黑树,则遍历链表
3.3.1.若遍历链表的下一个节点为空(同时会赋值给节点e),也即链表中没找到目标点, 则直接在链表尾部新建节点并插入到尾部后再判断链表节点数是否超过8个,若超过 则调用treeifybin方法将链表转为红黑树,然后退出循环
3.3.2.若在链表的非第一个节点找到目标节点,则退出循环
3.3.3.若没找到目标节点,则指针后续,继续循环遍历
4. 若节点e不为空,也即在链表找到相同的目标节点,则替换,也即覆盖该节点的value值,并返 回旧的value值
5. 若插入节点后,数组table的size大于阈值,则扩容
红黑树的puTreeValue操作:
//todo
数组扩容:
jdk1.7分为两步:
1.扩容:创建一个长度为原数组两倍的Entry[]
2.ReHash:双循环遍历数组,再把元素hash到新数组中(for遍历原数组Entry元素,while遍历数组元素上的链表)
jdk1.7数组+链表的数据结构在put时采用头插法,扩容时在多线程的场景下存在死循环场景
解决办法:
1.使用线程安全的ConcurrentHashMap代替HashMap(推荐使用)
2.使用线程安全的HashTable,但效率较低(不推荐使用)
3.使用synchronized或Lock加锁,保证线程安全,也会影响性能(不推荐使用)
jdk.18
jdk1.8使用数组+链表/红黑树的数据结构,put时采用改用尾插法,彻底优化了扩容时多线程的场景下的死循环问题
二、ConcurrentHashMap
1.底层数据结构:
JDK1.7的ConcurrentHashMap底层采用:Segments数组+HashEntry数组+链表
JDK1.8的ConcurrentHashMap底层采用:Node数据+链表+红黑树
2.实现线程安全的方式:
在JDK1.7中ConcurrentHashMap采用分段锁实现线程安全。
在JDK1.8中ConcurrentHashMap采用synchronized和CAS来实现线程安全。
三、TreeMap
非线程安全,基于红黑树实现,实现了SortMap接口,有序,默认是按键(key)的升序排列。
四、LinkendHashMap
非线程安全,继承HashMap,基于hashMap和双链表实现,有序,默认为插叙顺序
五、Hashtable
线程安全,源码中的get、put等方法都是synchronized修饰的同步方法,保证了线程安全,如get为同步方法,执行get方法会把hashtable中的的数据全部锁住,数据量越大,效率越低
HashMap的put方法源码:
public V put(K key, V value) {
//实际会调用到以下5个参数的重载方法
return putVal(hash(key), key, value, false, true);
}
/**
* 实现了Map的put和其它相关方法
*
* @param key的hash
* @param key
* @param value
* @param onlyIfAbsent true的话不改变当前key的值
* @param evict 驱逐 这个参数是给定长的LinkedHashMap使用的,可以实现达到最大长度后移除元素
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;//n 桶的长度,待插入元素定位到的桶的位置索引
//如果table是空或者长度是0,初始化table数组(桶),
if ((tab = table) == null || (n = tab.length) == 0)
//首次插入:此条件第一次调用put的时候,会调用,实际tab初始化的过程在resize()函数中实现
n = (tab = resize()).length;//tab的长度,前面分析过tab的长度是2^n次方
if ((p = tab[i = (n - 1) & hash]) == null)
//首次插入or非首次插入头结点为空:
//首次插入的符合此条件,i = (n - 1) & hash i其实就是计算定位出来的当前待插入元素的索引
tab[i] = newNode(hash, key, value, null);//在桶中存入此节点作为头节点
else {
//非首次插入头节点非空:hash算法(i = (n - 1) & hash)定位到桶位置中存在元素
Node<K,V> e; K k;
//判定是否等于第一个节点P
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//如果节点的hash和键的值等于链表中第一个节点的值,则将e指向该节点
e = p;//存储元素
else if (p instanceof TreeNode)//p节点是红黑树节点,调用红黑树节点插入
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//不等于第一个节点的值,遍历链表继续查找
for (int binCount = 0; ; ++binCount) {
//顺序遍历链表,找到空节点,插入
if ((e = p.next) == null) {
//在当前节点后面插入新节点
p.next = newNode(hash, key, value, null);
//插入后判断是否要将链表转化成红黑树,节点元素个数大于等于8的时候
//新追加了一个元素,所以实际元素个数的判断TREEIFY_THRESHOLD - 1=7
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//转换成红黑树
treeifyBin(tab, hash);
break;
}
//在链表的非第一个节点中,找到重复key的元素
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;//p的应用指针后移1个,实际就是在遍历
}
}
//在链表中的第一个位置或是后续位置找到相同的key
if (e != null) { // existing mapping for key
//要插入的键值的键已经存在,更新value
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;//用于并发修改的一个异常判断,基本非线程安全集合类都有类似操作
if (++size > threshold)//大于阈值需要进行扩容
resize();
afterNodeInsertion(evict);//主要用于LinkedHashMap使用,这里不做说明了
return null;
}