文章目录
开头
借用网友的图片,这是hashmap的整体结构,数组+链表(jdk>1.8时加入红黑树)
本文使用的jdk版本为1.8.0_301,内容较jdk1.7版本改变很大
看文章的时候不建议完全看我的注释,建议先自己分析,遇到卡壳的地方来看一下(我也是这么学的)。
看的话,建议从头到尾看,因为开头写的比较详细
关于HashMap的基础部分,看看这个挺好的
https://zhuanlan.zhihu.com/p/127147909
HashMap类
hash方法
//根据key,算出hash值,后续将hash%n得到key在数组中的位置
static final int hash(Object key) {
int h;
//key.hashCode(),每个基本类型实现了自己的hashCode方法,比如String.hashCode(),可以自己去看一看
//java中>>>代表无符号右移,忽略符号位,空位都以0补齐
//h^(h>>>16)是h的低16位和高16位相与。如果容量不大很少用到hash的高位,这里均衡用了一下
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
总结:
1.设置的key需要实现hashCode方法
2.hashmap中最终的hash是高16位和低16位相与的结果
put方法
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;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//(n-1)&hash = hash%n 其中n是数组的长度,也就是说通过hash定位到数组中的位置
//对计算机来说,&操作相比%时间更短
if ((p = tab[i = (n - 1) & hash]) == null)
//数组中没有值就插入一个新的Node
tab[i] = newNode(hash, key, value, null);
//发生hash碰撞,解决
else {
Node<K,V> e; K k;
//p为原数组中的结点,与新加进来的结点进行比较:
//hash相同,同时key也完全相同,则把当前结点负值给临时变量e,后续修改value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果原结点p,已经是红黑树的结点,那么就将当前结点加入到p所在的红黑树中,e作为返回值
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//否则既不是原结点修改value也不是红黑树,则为添加到链表中,同时进行计数
//如果加入后节点值>=8则转换为红黑树
//如果在链表中找到了key相同的结点,进行value替换
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//添加到空结点的后面
p.next = newNode(hash, key, value, null);
//计数超过8-1,则链表转化成红黑树
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;
}
}
//命中不为空,则赋值,并返回
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//命中为空,则为修改hashmap的结构,修改计数加一
++modCount;
//修改结构后判断大小,是否进行resize
if (++size > threshold)
resize();
//Callbacks to allow LinkedHashMap post-actions
afterNodeInsertion(evict);
return null;
}
总结
put分为两种:
1.命中hashmap中的某个key,则修改value
2.未命中,添加结点。添加结点时如果总结点数大于8,并且数组长度大于64会将链表转化成红黑树
如果节点数大于8,但是数组长度小于64,会先考虑扩容。因为hash在小于64的时候效率最高
get方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
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) {
//数组中存在hash相同
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;
}
总结
1.数组中结点是否命中
2.判断是否是红黑树,并查找结点
3.链表中查找结点
resize方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//threshold表示当HashMap的size大于threshold时会执行resize操作。
//threshold=capacity*loadFactor
//在HashMap初始化的时候如果填写初始化容量initialCapacity,则会初始化threshold,值为this.threshold = tableSizeFor(initialCapacity);否则threshold的值为0
//后文称之为扩容阈值
int oldThr = threshold;
int newCap, newThr = 0;
//旧容量大于0
if (oldCap > 0) {
//旧数组容量已经大于最大容量了,阈值设置到更大,
//HashMap.MAXIMUM_CAPACITY = 1 << 30 值为0x40000000
//Integer.MAX_VALUE = 0x7fffffff
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//旧数组容量扩大二倍还没到最大容量,就把数组扩大二倍,同时阈值扩大二倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//表示旧容量是0 但是旧阙值却大于零,就扩容新的容量为旧的阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//旧容量为0 旧阈值为0 则初始化
else { // zero initial threshold signifies using defaults
//初始容量 1<<4
newCap = DEFAULT_INITIAL_CAPACITY;
//初始阈值 1<<4 * 0.75
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//新阈值为0时,设置新阈值为新容量 * 新加载因子
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
//考虑到新容量或者新阈值是否比最大容量大,如果是的话,设置最大容量为0x7fffffff
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//扩容结束
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//重新建立hash数组
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;
//e没有下一个结点,就老哥一个,直接重新散列
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果在旧哈希表中,这个位置是树形的结果,就要把新hash表中也变成树形结构
//通过split函数进行拆分
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//链表的情况赋值给new tab
else { // preserve order
//下面算法很微妙
//定义两个链表lo和hi,分别设置头指针和尾指针
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//将原链表拆分成两个链表
//根据(e.hash & oldCap) == 0来区分是加入lo链表还是hi链表
//扩容2倍后Node要么添加在j上,要么添加在j+oldCap上具体原理,参考
//https://segmentfault.com/a/1190000015812438
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);
//如果lo链表非空, 我们就把整个lo链表放到新table的j位置上
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//如果hi链表非空, 我们就把整个hi链表放到新table的j+oldCap位置上
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
总结
resize发生在table初始化, 或者table中的节点数超过threshold值的时候, threshold的值一般为负载因子乘以容量大小.
每次扩容都会新建一个table, 新建的table的大小为原大小的2倍.
扩容时,会将原table中的节点re-hash到新的table中, 但节点在新旧table中的位置存在一定联系: 要么下标相同, 要么相差一个oldCap(原table的大小).
这也是cap为2的倍数的好处之一
remove方法
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//这块跟get方法一样,先找结点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//找到之后要删除
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
总结:先找结点,然后按照类型执行对应的方法,进行删除
TreeNode类
基本结构
//继承LinkedHashMap.Entry,LinkedHashMap底层结构是 HashMap + 双向链表
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
treeifyBin方法(用链表构建红黑树)
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//数组的长度小于64先扩容,而不是只要长度大于8就转化为树
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//否则再转化
//找到数组中的结点
else if ((e = tab[index = (n - 1) & hash]) != null) {
//hd是头结点,tl是临时结点
TreeNode<K,V> hd = null, tl = null;
do {
//将Node结点替换成TreeNode结点
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
//构建双向链表
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
//转化成树
hd.treeify(tab);
}
}
//树的构建
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
总结:转化会先判断tab长度,小于64先进行扩容
split(拆分红黑树)
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
//在这之前的逻辑是将红黑树每个节点的hash和一个bit进行&运算,
//根据运算结果将树划分为两棵红黑树,lc表示其中一棵树的节点数
if (loHead != null) {
//树的结点数<=6重新构建红黑树(前提是先调用resize方法)
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
总结:resize的时候会执行红黑树的拆分,拆分成两部分,拆分后小于等于6的才会转化成链表
关于hashCode方法贴出几个常见类的
String.hashCode
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
Integer.hashCode
public static int hashCode(int value) {
return value;
}
ArrayList.hashCode(继承AbstractList)
HashMap中的key最好不要使用可变类,比如ArrayList。
因为HashMap中的映射是基于key.hashCode(),如果key改变则key.hashCode()也会改变,导致最终映射错误
public int hashCode() {
int hashCode = 1;
for (E e : this)
hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
return hashCode;
}
线程安全的ConcurrentHashMap
hashmap不是线程安全的。
提到线程安全不得不提到Hashtable,Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
CAS + synchronized
在JDK1.7之前,ConcurrentHashMap是通过分段锁机制来实现的,所以其最大并发度受Segment的个数限制。因此,在JDK1.8中,ConcurrentHashMap的实现原理摒弃了这种设计,而是选择了与HashMap类似的数组+链表+红黑树的方式实现,而加锁则采用CAS和synchronized实现。
cas是什么呢,其实Java并发框架的基石一共有两块,一块是本文介绍的CAS,另一块就是AQS
这里引用一下别人的话
CAS机制是一种数据更新的方式。在具体讲什么是CAS机制之前,我们先来聊下在多线程环境下,对共享变量进行数据更新的两种模式:悲观锁模式和乐观锁模式。
悲观锁更新的方式认为:在更新数据的时候大概率会有其他线程去争夺共享资源,所以悲观锁的做法是:第一个获取资源的线程会将资源锁定起来,其他没争夺到资源的线程只能进入阻塞队列,等第一个获取资源的线程释放锁之后,这些线程才能有机会重新争夺资源。synchronized就是java中悲观锁的典型实现,synchronized使用起来非常简单方便,但是会使没争抢到资源的线程进入阻塞状态,线程在阻塞状态和Runnable状态之间切换效率较低(比较慢)。比如你的更新操作其实是非常快的,这种情况下你还用synchronized将其他线程都锁住了,线程从Blocked状态切换回Runnable华的时间可能比你的更新操作的时间还要长。
乐观锁更新方式认为:在更新数据的时候其他线程争抢这个共享变量的概率非常小,所以更新数据的时候不会对共享数据加锁。但是在正式更新数据之前会检查数据是否被其他线程改变过,如果未被其他线程改变过就将共享变量更新成最新值,如果发现共享变量已经被其他线程更新过了,就重试,直到成功为止。CAS机制就是乐观锁的典型实现。
使用
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ConcurrentHashMapDemo {
private final ConcurrentHashMap<Integer,String> conHashMap = new ConcurrentHashMap<Integer,String>();
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(3);
ConcurrentHashMapDemo ob = new ConcurrentHashMapDemo();
service.execute(ob.new WriteThreasOne());
service.execute(ob.new WriteThreasTwo());
service.execute(ob.new ReadThread());
service.shutdownNow();
}
class WriteThreasOne implements Runnable {
@Override
public void run() {
for(int i= 1; i<=10; i++) {
conHashMap.putIfAbsent(i, "A"+ i);
}
}
}
class WriteThreasTwo implements Runnable {
@Override
public void run() {
for(int i= 1; i<=5; i++) {
conHashMap.put(i, "B"+ i);
}
}
}
class ReadThread implements Runnable {
@Override
public void run() {
Iterator<Integer> ite = conHashMap.keySet().iterator();
while(ite.hasNext()){
Integer key = ite.next();
System.out.println(key+" : " + conHashMap.get(key));
}
}
}
}
下篇文章接着写并发吧,太长了
参考
https://zhuanlan.zhihu.com/p/127147909
https://segmentfault.com/a/1190000015812438
https://tech.meituan.com/2016/06/24/java-hashmap.html
https://www.cnblogs.com/aaabbbcccddd/p/14849064.html
https://www.cnblogs.com/54chensongxia/p/12160085.html
https://pdai.tech/md/java/thread/java-thread-x-juc-collection-ConcurrentHashMap.html
红黑树没有基础
https://www.zhihu.com/question/312327402
https://www.jianshu.com/p/e136ec79235c