JDK1.7 HashMap
底层使用的是数组加链表的形式
链表的定义
一共有四个值 key , value , hash值 ,next指针
class Entry<K,V>{
//存四个值
K k;
V v;
int hash;
Entry<K,V> next;
public Entry(K k, V v, int hash, Entry<K, V> next) {
this.k = k;
this.v = v;
this.hash = hash;
this.next = next;
}
public K getKey() {
return k;
}
public V getValue() {
return v;
}
}
如何插入?
采用头插法
根据key,计算hash值index,到数组下标为index的地方看看是不是空,如果是空,直接插入
table[index] = new Entry<>(k, v, index, null); -这里的next指针指向null
如果不为空,直接头插
table[index] = new Entry<>(k, v, index, entry); -这里的next指针指向index下链表的原本头结点
public V put(K k, V v) {
int index = hash(k);
Entry<K,V> entry = table[index];
if(null ==entry){
table[index] = new Entry<>(k, v, index, null);
size++;
}else{
table[index] = new Entry<>(k, v, index, entry);
}
return table[index].getValue();
}
如何取值?
根据key,计算hash值index,到数组下标为index的地方看看是不是空,如果是空,直接返回null,否则顺序查找链表
public V get(K k) {
//如果没有存储数据那size 为0,也不用查了,直接返回null
if(size ==0){
return null;
}
int index = hash(k);
Entry<K, V> entry = findValue(table[index], k);
//通过index 找打这个对象
return entry==null?null:entry.getValue();
}
private Entry<K,V> findValue(Entry<K,V> entry,K k) {
//存的可能是数值类型,也可能是字符串类型
if (k.equals(entry.getKey()) || k == entry.getKey()) {
return entry;
//如果不相等,估计这个节点是个链表,判断它next 数据是否匹配
} else {
if(entry.next !=null){
//用到递归,在链表里面一直查询这个k,值是否相等
return findValue(entry.next,k);
}
}
return null;
}
如何扩容?、
将原来的数组变大,取出A,放到新HashMap对应位置,然后取出A.next即B,头插法加入新HashMap,依此类推,发现一个位置上的结点全部颠倒了
HashMap的问题
①死循环,为什么会产生死循环?
面试突击16:为什么HashMap会产生死循环?_哔哩哔哩_bilibili
如上图所示。线程T1,T2都想对HashMap扩容,T1,T2同时指向A,T1.next,和T2.next都指向B,T2突然阻塞,T1先对其进行扩容,扩容之后的链表为C->B->A。此时,T2被唤醒,也进行扩容(注意此时的链表是C->B->A,但是线程T2不知道,T2认为还是A->B->C),先将A插入,再头插法插入A.next即B,然后插入B.next(由于T1已经扩容,B.next是A),继续查A.next又是B,出现死循环
处理方法
②数据覆盖问题
1.线程T1进行添加时,判断某个位置可以插入元素,但还没有真正的进行插入操作,自己时间片就用完了
2.线程T2也执行添加操作,并且T2产生的哈希值和T1相同,也就是T2即将要存储的位置和T1相同,因为此位置尚未插入值(Tl线程执行了一半)于是T2就把自己的值存入到当前位置了
3.T1恢复执行之后,因为非空判断已经执行完了,它感知不到此位置已经有值了,于是就把自己的值也插入到了此位置,那么T2的值就被覆盖了
解决方法
③无序性问题
这里的无序性问题指的是HashMap添加和查询的顺序不一致
解决方法:使用LinkedHashMap
红黑树
红黑规则
- 节点不是黑色,就是红色(非黑即红)
- 根节点为黑色
- 叶节点为黑色(叶节点是指末梢的空节点
Nil
或Null
) - 一个节点为红色,则其两个子节点必须是黑色的(根到叶子的所有路径,不可能存在两个连续的红色节点)
- 每个节点到叶子节点的所有路径,都包含相同数目的黑色节点(相同的黑色高度)
添加一个节点时,如果不符合红黑树的规则,那么调整的方法有:改变颜色,左旋,右旋
JDK1.8 HashMap
简单来说HashMap的元素添加流程是,先将key值进行hash得到哈希值,根据哈希值得到元素位置,判断元素位置是否为空,如果为空直接插入,不为空判断是否为红黑树,如果是红黑树则直接插入否则判断链表是否大于8,且数组长度大于64,如果满足这两个条件则把链表转成红
黑树,然后插入元素,如果不满足这两个条件中的任意一个,则遍历链表进行插入。
为什么要将链表转红黑树?
主要是出于性能的考量,因为链表超过一定长度之后查询效率就会很低,它的时间复杂度是O(n),而红黑树的时间复杂度是0(logn),因此引入红黑树可以加快HashMap在数据量比较大的情况下的查询效率
一些参数的意义:
DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
threshold:扩容的临界值 = 容量*加载因子:16 * 0.75 => 12
TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64
TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
首先调用构造器,默认加载因子是0.75。
下面是调用put(K,V) 方法:
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; Node<K,V> p; int n, i;
//第一次调用put时,此时数组为空。size方法见下方代码块
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)//i是新元素插在数组中的位置,如果这个位置是null直接插入
tab[i] = newNode(hash, key, value, null);
else { //如果该位置有元素:
Node<K,V> e; K k;
//下面的if是指添加的元素与该位置原有的元素hash值,key值都相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //将改位置的元素p复制给e
else if (p instanceof TreeNode) //如果该链表是以红黑树的结构存储
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { //如果不存在hash,key都相等且 不用红黑树的结构存储
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//如果该链表的第一个元素后面没有元素
p.next = newNode(hash, key, value, null);//直接在末尾插入,next置空
if (binCount >= TREEIFY_THRESHOLD - 1) // 如果当前链表的长度大于等于7,插入一个元素后将达到最大值,那么转化成红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && //链表从头开始遍历,如果遍历时发现有元素与被插入元素的hash,key值相等,进入下面的if :替换value值
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { //如果出现hash值与key值相等的情况,将要添加的元素的value替换原来元素的value
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; //复制一份底层数组
int oldCap = (oldTab == null) ? 0 : oldTab.length; //oldCap:数组的长度
int oldThr = threshold; //数组的临界值,默认是0
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//数组的长度double,如果原来的数组长度大于16,临界值也double
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // 如果数组的长度为0
newCap = DEFAULT_INITIAL_CAPACITY; //创建长度为16的数组
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//默认临界值12
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // 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;
}
}
}
}
}
return newTab;
}
ConcurrentHashMap
ConcurrentHashMap是HashMap的多线程版本,HashMap在并发操作时会有各种问题,比如死循环问题、数据覆盖等问题;
而这些问题,只要使用ConcurrentHashMap就可以完美解决了
那问题来了,ConcurrentHashMap是如何保证线程安全的?
4、ConcurrentHashMap
可以通过ConcurrentHashMap 和 Hashtable来实现线程安全;Hashtable 是原始API类,通过synchronize同步修饰,效率低下;ConcurrentHashMap 通过分段锁实现,效率较比Hashtable要好;
ConcurrentHashMap的底层实现:
JDK1.7的 ConcurrentHashMap 底层采⽤ 分段的数组+链表 实现;采用 分段锁(Sagment) 对整个桶数组进⾏了分割分段(Segment默认16个),每⼀把锁只锁容器其中⼀部分数据,多线程访问容器⾥不同数据段的数据,就不会存在锁竞争,提⾼并发访问率。
JDK1.8的 ConcurrentHashMap 采⽤的数据结构跟HashMap1.8的结构⼀样,数组+链表/红⿊树;摒弃了Segment的概念,⽽是直接⽤ Node 数组+链表+红⿊树的数据结构来实现,通过并发控制 synchronized 和CAS来操作保证线程的安全。