目录
何为Hashmap
利用hash表原理进行数据存储的集合。线程不安全。
jdk1.7采用数组加链表进行存储,当遇到冲突时,利用链表存储冲突的数据。利用头插法操作链表,hashmap进行数据查询的时间复杂度取决于链表长度,时间复杂度为O(n)。在高并发的情况下会出现循环链表死循环和数据丢失。
jdk1.8采用数组加链表加红黑树进行存储,当遇到冲突时,当链表长度大于8(TREEIFY_THRESHOLD)时会把链表存储转换为红黑树进行存储。当通过删除或者分裂操作导致树中节点个数小于6时,消除树结构。
利用尾插法操作链表,hashmap进行数据查询的时间复杂度取决于红黑树,时间复杂度为O(logn)。在高并发的情况下会出现数据覆盖问题。
HashMap1.7put具体分析
大体流程分析
1.当一个函数调用put方法时,如何hashmap没有被初始化,对其进行初始化,数组初始化长度为16。计算key值得hash值,如果有值且key一样,覆盖value.如果hash值位置无值,执行addEntry。如果hash值位置已经有值,一直执行e.next,直到该hash值链表末尾,执行addEntry
2.在addEntry方法中
2.1 插入时发现map中的键值对个数大于阈值并且在插入当前值时发生了碰撞,执行resize方法。在resize 方法中,判断数组是否等于扩容的最大值,等于时修改扩容阈值为扩容最大值。创建一个entry新数组,其长度为原数组长度的两倍。执行transfer方法。transfer方法对原数组进行循环,利用头插法把原数组中的数据插入新的数组中,最终的效果为同一位置的链表顺序翻转。返回addEntry执行table=new table,createEntr方法
例如,假设元素组长度为2,利用3,7,5 往其中插入,在数组index=1处形成链表7->3后,把5再往其中插入,触发resize 方法,数组长度变为4,在index=3处形成链表3->7。再往新数组index=1中插入5
2.2 插入时发现没有满足resize条件,执行createEntry方法
3.在createEntry中把数组index处的entry用一个新建的Entry e来存放,执行Entry方法,在Entry方法中让要插入的entry的next指向e(头插法)。
代码所涉及方法
public V put(K key, V value) {
//判断当前Hashmap(底层是Entry数组)是否存值(是否为空数组)
if (table == EMPTY_TABLE) {
inflateTable(threshold);//如果为空,则初始化
}
//判断key是否为空
if (key == null)
return putForNullKey(value);//hashmap允许key为空
//计算当前key的哈希值
int hash = hash(key);
//通过哈希值和当前数据长度,算出当前key值对应在数组中的存放位置
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果计算的哈希位置有值(及hash冲突),且key值一样,则覆盖原值value,并返回原值value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//存放值的具体方法
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//1、判断当前个数是否大于等于阈值
//2、当前存放是否发生哈希碰撞
//如果上面两个条件否发生,那么就扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容,并且把原来数组中的元素重新放到新数组中
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//判断是否有超出扩容的最大值,如果达到最大值则不进行扩容操作
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
// transfer()方法把原数组中的值放到新数组中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//设置hashmap扩容后为新的数组引用
table = newTable;
//设置hashmap扩容新的阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
transfer()在实际扩容时候把原来数组中的元素放入新的数组中
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//通过key值的hash值和新数组的大小算出在当前数组中的存放位置
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
扩容造成死循环
https://www.cnblogs.com/developer_chan/p/10450908.html(这篇博客写的很详细)
https://blog.youkuaiyun.com/q503385724/article/details/87910929(JVM变量存储讲解)
假设数组old长度为2,数组中在index=1处存在链表7->3
现在存在两个线程同时要往old数组中插入键值对(5,null)
线程A执行插入方法,在addEntry中触发resize方法,对old数组进行循环,当e=7时,新数组index=3处为null ,即7.next=null,执行 newTable[3]=7时把该线程A挂起,此时A中java栈保存着newTable变量的引用,next=7->3,e=7,7.next=null,newTable[3]=7, 该newTable变量的引用为线程A私有
线程B执行插入方法,在addEntry中触发resize方法,对old数组进行循环。在index=3处形成链表3->7。把新数组赋值给table,新的table值存在java堆中,table值会被刷新的主存被所有线程共享,再往新数组index=1中插入5。
此时切换到线程A,执行e=next, 把e变化为3,由于B对table进行了修改,此时next=3.next,在新修改的 table中,3.next=7,所以e=7, 7.next=3,newTable[3]=7,e=3,线程A进入死循环
HashMap 1.8
红黑树
二叉查找树:中序遍历后得到一个节点递增的有序序列。
红黑树:一种含有红黑节点并且能自平衡的二叉查找树
性质
-
性质1:每个节点要么是黑色,要么是红色。
- 性质2:根节点是黑色。
- 性质3:每个叶子节点(NIL或者NULL)是黑色。
- 性质4:每个红色结点的两个子结点一定都是黑色。
- 性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。
红黑树节点定义
这个节点继承了LinkedHashMap.Entry,因为在putVal方法中回调了LinkedHashMap的方法。
而这个Entry类实际上也是继承了HashMap.Node类,所以每个TreeNode节点中含有Node.next 指针,所以每个TreeNode节点也是Node节点,所构建的链表是一个双向链表,要构建树时利用next指针得到链表中每个值
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 父节点
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // 指向前置节点的指针,删除Node时需要修改链接
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
红黑树构建及调整流程
/**
* tab中已经存在至少8个节点,在x为要插入的当前节点,p为从根节点开始遍历,
* 计算出x要插入的p的位置,如果为空,插入x,那么p就是x的父节点,x为p的子节点,如果不能插入,则继续循环,循环到可以插入,
* 当插入完成之后,退出当前查找插入父节点的循环
* 执行x=next,链表中的下一位数据进行插入
*/
final void treeify(Node<K, V>[] tab) {
TreeNode<K, V> root = null;
for (TreeNode<K, V> x = this, next; x != null; x = next) { //利用tab的next指针对tab进行构建红黑树
next = (TreeNode<K, V>) x.next;
x.left = x.right = null;
// 初始化root
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;;) { //利用p查找插入节点的位置,利用hash判断,如果hash一样,利用key和 类名判断
int dir, ph;
K pk = p.key;
/**
* 当在当前结点的右节点时 dir=-1 当在当前结点的左节点时 dir=1 当hash值相同时比较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);// 主要是比较当key为null是时,p.k与k的hash值大小
TreeNode<K, V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) { //p=p.right或p.left
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}//插入完成,退出当前循环
}
}//x=next 插入链表中的下一值
moveRootToFront(tab, root);
}
/**
*
* @param root
* @param x
* xp x的父节点 xpp x的祖父节点
* 以红色节点为目标节点进入插入,每个插入的节点默认红色,以此红色节点调整父节点,并接着从祖父节点开始调整
* 1 当父节点为祖父节点左节点:
* 1.1父节点为红色时且叔叔节点为红色,直接插入
* 1.2父节点为红色时且叔叔节点为黑色或者不存在时:
* 1.2.1插入节点为父节点的右节点,左旋,xp为x左节点 让 xpp,xp,x在一条线上,再右旋 ,xpp变成x右节点
* 1.2.2插入节点为父节点的左节点,根据xpp进行右旋,xpp变成x右节点
* 2 当父节点为祖父节点右节点:
* 2.1 父节点为红色时且叔叔节点为红色,直接插入
* 2.2 父节点为红色时且叔叔节点为黑色或者不存在时:
* 2.2.1 插入节点为父节点的左节点,右旋,xp为x左节点 让 xpp,xp,x在一条线上,再左旋 ,xpp变成x左节点
* 2.2.2要插入节点为父节点的右节点,左旋,xpp变成x左节点
*
* 当旋转执行完成之后, x等于xp,原本要插入的节点=x.parent 或者 x没有发生改变 x.parent 指向原本的祖父节点
*
* for (TreeNode<K, V> xp, xpp, xppl, xppr;;) 继续循环,
* 直到x节点变成父节点或者父节点为黑色 返回root
* @return
*/
static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root, TreeNode<K, V> x) {
x.red = true;
for (TreeNode<K, V> xp, xpp, xppl, xppr;;) {
if ((xp = x.parent) == null) { //如果x是根节点
x.red = false;
return x;
} else if (!xp.red || (xpp = xp.parent) == null) // xp为黑色或者xp为根节点时返回
return root;
if (xp == (xppl = xpp.left)) {// 1.xp为红色,xp为父节点的左节点
if ((xppr = xpp.right) != null && xppr.red) {// 1.1如果xppr的右节点不为空且为红色时,设置xpp的右节点为黑色,左节点为黑色,xpp为红色,调整完当前节点的上一层节点后,从xpp开始调整
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
} else { // 1.2如果xppr为黑色或者xppr不存在(叔叔节点)
if (x == xp.right) {// 如果x节点为xp的右节点,进行一次左旋,用要插入的节点代替父节点,父节点为左节点
root = rotateLeft(root, x = xp);// 传入根节点和 xp x.parent=pp
xpp = (xp = x.parent) == null ? null : xp.parent; // x 变成原本父节点
// xpp为原本xp的父节点
}
if (xp != null) { // xp为新插入的节点
xp.red = false; // 新插入xp设为黑色或者就是上个if没执行,父节点变成 黑色
if (xpp != null) { // 原本的祖父节点变成红色
xpp.red = true;
root = rotateRight(root, xpp);//传入root节点和原本的祖父节点
}
}
}
} else {//2.xp为红色,xp为xpp.right
if (xppl != null && xppl.red) {//2.1如果xppl不空且为红色,x的叔叔节点与父节点都为黑色,祖父节点为红色
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp; //x变成祖父节点
} else { //2.2如果xppl空了或者xppl为黑色,空了及父节点叔叔节点颜色不一致说明该结构不合理
if (x == xp.left) { //要插入节点为父节点的左节点,右旋 ,
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false; // 如果没有执行上个if,父节点变成黑色,执行了上个if,插入节点变成黑色
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
/**
* 左旋主要实现把插入的节点(本为父节点的右节点)的左节点变成父级节点,
* 父节点的右节点变成插入节点的左节点 执行这个方法时,x为xp的右节点,即r=x
* pp 为p.parent
*
* @param root
* @param p
* @return
*/
static <K, V> TreeNode<K, V> rotateLeft(TreeNode<K, V> root, TreeNode<K, V> p) {
TreeNode<K, V> r, pp, rl;
if (p != null && (r = p.right) != null) {// r,p都存在 p比r小
if ((rl = p.right = r.left) != null)// 1.r的左节点变为父节点的右节点 p.right=r.left,rl=p.right,rl=r.left
rl.parent = p;
// r.parent=p.parent,pp=p.parent为null,当前结点设置为黑色和根节点
if ((pp = r.parent = p.parent) == null)// 2.1
(root = r).red = false;
else if (pp.left == p) // 2.2 如果p(要插入节点r的父节点)节点为父节点的左节点
pp.left = r;
else
pp.right = r;// 2.3 r节点变成父父节点的右节点
r.left = p; // 要插入节点的左节点变成p,p的父节点变成r
p.parent = r;
}
return root; // 返回根节点,看根节点是否发生改变
}
/**
* 右旋就是对插入节点(为连接到的父节点的左节点)与其父级节点进行替换,进行旋转,
* 使得父节点变成原本节点的右节点,父节点的左节点变成插入节点的右节点
* @param root
* @param p 新插入节点的父节点 l为新插入节点的原本父节点
* @return
*/
static <K, V> TreeNode<K, V> rotateRight(TreeNode<K, V> root, TreeNode<K, V> p) {
TreeNode<K, V> l, pp, lr;
if (p != null && (l = p.left) != null) {//p,l 存在 p比l大
if ((lr = p.left = l.right) != null) //l的右节点变为父节点的左节点
lr.parent = p;
if ((pp = l.parent = p.parent) == null)//如果祖父节点p不为根节点,l取代父节点的位置
(root = l).red = false;
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
l.right = p; //l的右节点指向父节点
p.parent = l;
}
return root;
}
https://www.cnblogs.com/oldbai/p/9888370.html 带图解释左旋代码,还比较清晰
Map 其他代码解析
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
keySet方法(视图方法)
HashMap 继承了一个AbstractMap类,实现了Map接口。在AbstractMap类中最重要的是keySet方法。keySet是一个视图方法,在keySet中并没有实际的查询map中的key保存到一个set中,而是利用重写的Iterator方法,当要获得keySet 然后对每个key值使用Iterator时,才对map中的table进行查询。
tableSizeFor
通过位移把cap的低位全变为1,最后获得最接近比cap大的2的次幂数组容量。例如cap=9,其补码为0101最后获得的n的补码为0111,n+1最后返回的capcity补码即为1000
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
hash
key.hashcode左移16位异或key.hashcode,这样做相较1.7较好保存了高位信息,这么做可以在数组table的length比较小的时候,也能保证高低位参与到hash计算中,又不会有很大的消耗
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
resize
初始化:在resize 方法中会根据两种构造函数分别创建不同长度的table
1.使用默认构造函数,会进入else条件,创建一个数组长度为16,阈值为12的新数组
2.使用带容量的构造函数,进入else if ,创建一个数组长度为 tableSizeFor(initialCapacity) ,
阈值为tableSizeFor(initialCapacity)*loadFactor的新数组
如果是扩容而不是初始化:数组长度变为原来两倍,阈值也变为原来两倍
https://blog.youkuaiyun.com/soga613/article/details/78958642 讲resize 讲的很不错
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; //首次初始化后table为null
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold; //使用默认构造器的threshold为0
int newCap, newThr = 0;
if (oldCap > 0) { //3.执行过 resize
if (oldCap >= MAXIMUM_CAPACITY) { //已经达到最大值,无法进行扩容
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) // threshold 与 table.length都将变成两倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // 2.initial capacity was placed in threshold
//使用带initialCapacity的构造器threshold=tableSizeFor(initialCapacity)
newCap = oldThr;
else { // 1.zero initial threshold signifies using defaults //使用默认无参数构造函数
newCap = DEFAULT_INITIAL_CAPACITY; //默认16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //默认计算出为12
}
if (newThr == 0) { //使用带initialCapacity的构造器进入这个if 把newThr变成threshold*loadFactor
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长创建一个新的table,实现初始化或者扩容
table = newTab;
if (oldTab != null) { //tabel!=null 把oldtab中的数据放入newtab
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null) // 如果e为oldTab[j]链表处的唯一值
newTab[e.hash & (newCap - 1)] = e; //把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 { //在oldTab[j]处发生了hash冲突
next = e.next;
if ((e.hash & oldCap) == 0) { //因为新的数组长度为旧数据长度的两倍,执行n-1之后,新的数组长度的二进制表示就比旧的数组长度的二进制表示多个高位1,如果利用e.hash值与新的数组长度进行与操作,如果最高位为0,那么e所在位置与未扩容之前一样
通过e.hash & oldCap计算出新增bit位到底为0还是为1 为0时表示e在扩容后位置没有发生改变
if (loTail == null) //loTail 中存储不需要移动的节点
loHead = e; //loHead 中存储冲突链表第一个不需要移动的节点,便于进行放置
else
loTail.next = e; //尾插法,链表顺序不发生改变
loTail = e;
}
else {
if (hiTail == null) //hiTail 中存储需要移动的节点
hiHead = e; //hiHead 中存储冲突链表第一个需要移动的节点,便于进行放置
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; //j+oldCap为新的链表中的位置
}
}
}
}
}
return newTab; //返回扩容完的tab
}
putVal
在冲突处的链表长度大于等于9时执行treeifyBin,在该方法中只有当table的长度(非entrySet个数,是数组长度)大于64,才会把Node链表转化为TreeNode链表,然后开始构建红黑树。如果table长度小于64 只会进行扩容来消除hash冲突
/**
*
* @param hash hash(key)
* @param key
* @param value
* @param onlyIfAbsent false
* @param evict true
* @return
*/
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;
if ((p = tab[i = (n - 1) & hash]) == null) //计算出 p在table中的位置,如果为空,直接插入
tab[i] = newNode(hash, key, value, null);
else {//在table[i]处已经有值
Node<K, V> e;
K k;
//如果要插入的数和p相同
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//table[i]处是一个红黑树
else if (p instanceof TreeNode)
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
else {//table[i]处是一个 链表
for (int binCount = 0;; ++binCount) {
if ((e = p.next) == null) { //把要插入的数据插入链表再判断链表中是否已经有8个节点.所以当实际进行treeifBin时链表中有9个结点
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash); //判断数组长度是否小于64,如果是,扩容,如果不是,把Node链表变成TreeNode链表
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))//如果e与要插入的节点相同,退出循环
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) //onlyIfAbsent 传值false
e.value = value; //覆盖value
afterNodeAccess(e); //调用LinkedHashMap中的afterNodeAccess,实现将操作过的节点放在最后一个
return oldValue;
}
}//e 为空才能执行这下面的操作 就是当执行第一个if发现table[i]出存在的节点为null,且要插入的节点也为null
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict); //删除最老的头节点
return null;
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果当前链表的长度小于64,扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {//如果数组长度大于64
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null); //创建新的 TreeNode节点
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null); //把Node链表转换为TreeNode链表
if ((tab[index] = hd) != null) //构建红黑树
hd.treeify(tab);
}
}
HashMap 1.8线程不安全
在HashMap1.8中采用尾插法插入时不会发生环形链表了,但是如果两个线程同时执行一个if ((p = tab[i = (n - 1) & hash]) == null) ;tab[i] = newNode(hash, key, value, null); 是可能发生数据丢失的。
参考链接
https://www.cnblogs.com/developer_chan/p/10450908.html(详解hashmap线程不安全 )
https://blog.youkuaiyun.com/insomsia/article/details/88367436(jdk1.7头插法解析)
https://blog.youkuaiyun.com/chen13333336677/article/details/99706702(jdk1.7扩容机制详解)
2342



被折叠的 条评论
为什么被折叠?



