【搞定Java基础-集合篇】第四篇 Java8-HashMap和ConCurrentHashMap

文章目录

一、Java8-HashMap源码【2倍扩容】
1.1 数据结构【数组+链表+红黑树,当put导致桶的数组长度>=64并且链表元素>8 个(插入的是第九个),会将链表转换为红黑树,降低查找的时间复杂度为 O(logN)。当红黑树节点数量<=6时,会转变为链表结构】【Java7: Entry,Java8 使用 Node来代表每个 HashMap 中的数据节点,,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。我们根据数组元素中,第一个节点数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树的】

1、 hashmap的成员属性分析
  • DEFAULT_LOAD_FACTOR很大时,桶的利用率较大的时候(注意可以大于1,因为冲突的元素是使用链表或者红黑树连接起来的),此时空间利用率较高,这也意味着一个桶中存储了很多元素,这时 HashMap 的 put、get 等操作代价就相对较大,因为每一个 put 或 get 操作都变成了对链表或者红黑树的操作,代价肯定大于O(1),所以说 DEFAULT_LOAD_FACTOR 是空间和时间的一个平衡点。
    • DEFAULT_LOAD_FACTOR较小时,需要的空间较大,但是 put 和 get 的代价较小;
    • DEFAULT_LOAD_FACTOR较大时,需要的空间较小,但是 put 和 get 的代价较大)
  • threshold 属性分析
    • threshold 也是比较重要的一个属性:创建 HashMap 时,该变量的值是:初始容量(2 的整数次幂),之后 threshold的值是 HashMap 扩容的门限值,即当前 Nodetable 数组的长度 * loadfactor。

      • 举个例子而言,如果我们传给 HashMap 构造器的容量大小为 9,那么 threshold 初始值为16,在向 HashMap 中 put 第一个元素后,内部会创建长度为 16 的 Node 数组,并且 threshold 的值更新为 16 * 0.75=12。具体而言,当我们一直往HashMap 中 put 元素的时候,如果put某个元素后,Node数组中元素个数为 13,此时会触发扩容(因为数组中元素个数> threshold了,即13 > threshold = 12),具体扩容操作之后会详细分析,简单理解就是,扩容操作将 Node 数组长度 * 2;并且将原来的所有元素迁移到新的 Node 数组中。
public class HashMap<K,V> extends AbstractMap<K,V>
			  implements Map<K,V>, Cloneable, Serializable {
 
	private static final long serialVersionUID = 362498820763181265L;
 
	// HashMap的初始容量为16,HashMap的容量指的是存储元素的数组大小,即桶的数量
	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
 
	// HashMap的最大的容量
	static final int MAXIMUM_CAPACITY = 1 << 30; 
 
	// 下面有详细解析
	static final float DEFAULT_LOAD_FACTOR = 0.75f;
	
	// 当某一个桶中链表的长度>=8时,链表结构会转换成红黑树结构,
	// 其实还要求桶的中元素数量>=64,后面会提到
	static final int TREEIFY_THRESHOLD = 8;
 
	// 当红黑树中的节点数量<=6时,红黑树结构会转变为链表结构
	static final int UNTREEIFY_THRESHOLD = 6;
 
	// 上面提到的:当Node数组容量>=64的前提下,如果某一个桶中链表长度>=8,
	// 则会将链表结构转换成红黑树结构
	static final int MIN_TREEIFY_CAPACITY = 64;
	// 我们往map中put的(k,v)都被封装在Node中,所有的Node都存放在table数组中

	transient Node<K,V>[] table;
 
	// 用于返回keySet和values
	transient Set<Map.Entry<K,V>> entrySet;
 
	// 保存map当前有多少个元素
	transient int size;
 
	// failFast机制,在讲解ArrayList和LinkedList一文中已经讲过了
	transient int modCount;
	
	int threshold; // 下面有详细讲解
 
	// 负载因子,见上面对DEFAULT_LOAD_FACTOR参数的讲解,默认值是0.75
	final float loadFactor;
}
2、HashMap 构造器源码分析:HashMap 是“懒加载”,在构造器中值保留了相关保留的值,并没有初始化 table 数组,当我们向 map 中 put 第一个元素的时候,map 才会进行初始化!
// 构造器:指定map的大小,和loadfactor
public HashMap(int initialCapacity, float loadFactor) {
	if (initialCapacity < 0)
		throw new IllegalArgumentException
		("Illegal initial capacity: " + initialCapacity);
	if (initialCapacity > MAXIMUM_CAPACITY)
		initialCapacity = MAXIMUM_CAPACITY;
	if (loadFactor <= 0 || Float.isNaN(loadFactor))
		throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
	   // 保存loadfactor
	   this.loadFactor = loadFactor;
 
	/* 注意,前面有讲tableSizeFor函数,该函数返回值:>= initialCapacity、
	返回值是2的整数次幂,并且得是满足上面两个条件的所有数值中最小的那个数 */
	this.threshold = tableSizeFor(initialCapacity);
}
 
// 只指定HashMap容量的构造器,loadfactor使用的是默认的值:0.75
public HashMap(int initialCapacity) {
	this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
 
// 无参构造器,默认loadfactor:0.75,默认的容量是16
public HashMap() {
	this.loadFactor = DEFAULT_LOAD_FACTOR; 
}
 
// 其他不常用的构造器就不分析了
3、HashMap 内部类——Node 源码分析
// Node是HashMap的内部类
static class Node<K,V> implements Map.Entry<K,V> {
	final int hash; 
	final K key;     // 保存map中的key
	V value;         // 保存map中的value
	Node<K,V> next;  // 单向链表
	
	// 构造器
	Node(int hash, K key, V value ,Node<K,V> next) {
		this.hash = hash;
		this.key = key;
		this.value = value;
		this.next = next;
	}
}

4、HashMap 中的 hash 函数:HashMap 允许 key 为null,null 的 hash 为 0。非 null 的 key 的 hash 高 16 位和低 16 位分别由:key 的 hashCode 高 16 位hashCode 的高 16 位异或 hashCode 的低16位组成。主要是为了增强 hash 的随机性,减少 hash & (n - 1) 的随机性,即减小 hash 冲突,提高 HashMap 的性能。
  • (因为如果直接使用 hashCode & (n - 1) 来计算 index,此时 hashCode 的高位随机特性完全没有用到,因为 n 相对于hashCode 的值很小,计算 index 的时候只能用到低 16 位。基于这一点,把 hashCode 高 16 位的值通过异或混合到hashCode 的低 16 位,由此来增强 hashCode 低 16 位的随机性。)
  static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
5、tableSizeFor 函数源码分析
static final int tableSizeFor(int cap) {
	// 举例而言:n的第三位是1(从高位开始数), 
	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;
}

在 new HashMap 的时候,如果我们传入了大小参数,这是 HashMap 会对我们传入的 HashMap 容量进行传到tableSizeFor 函数处理。这个函数主要功能是:返回一个数。这个数是大于等于 cap 并且是 2 的整数次幂的所有数中最小的那个,即返回一个最接近 cap(>= cap),并且是 2 的整数次幂的数。

具体逻辑如下:一个数是 2 的整数次幂,那么这个数减 1 的二进制就是一串掩码,即二进制从某位开始是一 串连续的1。所以只要对相应的掩码 +1 一定是 2 的整数次幂,这也是为什么 n = cap - 1 的原因。

举例而言,假设:n=00010000_00000000_00000000


n = 00010000_00000000_00000000    
 
n |= n >>> 1; //执行完后
// n = 00011000_00000000_00000000
 
n |= n >>> 2; // 执行完后
// n = 00011110_00000000_00000000
 
n |= n >>> 4; // 执行完后
// n = 00011111_11100000_00000000
 
n |= n >>> 8; // 执行完后
 
// n = 00011111_11111111_11100000
 
n |= n >>> 16; // 执行完后
// n = 00011111_11111111_11111111

返回 n + 1,(n + 1) >= cap、为2的整数次幂,并且是与 cap 差值最小的那个数。最后的 n+1 一定是 2 的整数次幂,并且一定是 >= cap。

整体的思路就是:如果 n 的二进制的第 k 为1,那么经过上面四个 ‘|’ 运算后 [0 ,k] 位都变成了1,即:一连串连续的二进制‘1’(掩码),最后 n+1 一定是 2 的整数次幂(如果不溢出)。

1.2 put【Java7先扩容再插入到链表最前面,Java8先插入再扩容,插入到链表最后面】【key和value可以为null】
1)第一次 put 值的时候,resize()初始化数组长度从 null 初始化到默认的 16 或自定义的初始容量,默认阈值 0.75
2)找到具体的数组下标(n - 1) & hash,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了
3)如果数组该位置有数据,判断是红黑树还是链表【插到链表最后面,重复就覆盖旧值,如果插入后个数是9个,则将链表转为红黑树】
4)如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容,每次扩容数组和阈值都为原来的 2 倍
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
 
// 第三个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 put 操作
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 1、第一次 put 值的时候,resize()初始化数组长度从 null 初始化到默认的 16 或自定义的初始容量
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2、找到具体的数组下标(n - 1) & hash,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
 
    else {// 3、数组该位置有数据,判断是红黑树还是链表【插到链表最后面,如果是第9个,链表转为红黑树】
        Node<K,V> e; K k;
        // 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是"相等",如果是,取出这个节点
        /* 为什么get和put先判断p.hash==hash,下面的if条件中去掉hash的比较逻辑也是正确?
		   因为hash的比较是两个整数的比较,比较的代价相对较小,
		   key是泛型,对象的比较比整数比较代价大,所以先比较hash,hash相等再比较key
		*/
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 如果该节点是代表红黑树的节点,调用红黑树的插值方法
        //如果已经存在该key的TreeNode,则返回该TreeNode,否则返回null
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 到这里,说明数组该位置上是一个链表
            for (int binCount = 0; ; ++binCount) {
                 /* 遍历到了链表最后一个元素,接下来执行链表的插入操作,先封装为Node,
				再插入p指向的是链表最后一个节点,将待插入的Node置为p.next,就完成了单链表的插入 */
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 9 个
                    // 会触发下面的 treeifyBin,也就是将链表转换为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果在该链表中找到了"相等"的 key(== 或 equals)
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 此时 break,那么 e 为链表中[与要插入的新值的 key "相等"]的 node
                    break;
                p = e;
            }
        }
        // e!=null 说明存在旧值的key与要插入的key"相等"
        // 对于我们分析的put操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
                /* 这个函数的默认实现是“空”,即这个函数默认什么操作都不执行,那为什么要有它呢?
			   这其实是个hook/钩子函数,主要要在LinkedHashMap(HashMap子类)中使用,
			   LinkedHashMap重写了这个函数。以后会有讲解LinkedHashMap的文章。*/
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 如果是第一次插入key这个键,就会执行到这里
    ++modCount;
    // 4、如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容
    if (++size > threshold)
        resize();
     // 这也是一个hook函数,作用和afterNodeAccess一样
    afterNodeInsertion(evict);
    return null;
}
1.3 HashMap 的 resize 函数源码分析 【重点中的重点】

面试谈到 HashMap 必考 resize 相关知识,整体思路介绍:

有两种情况会调用当前函数:

####### 1、之前说过 HashMap 是懒加载,第一次调用 HashMap 的 put 方法的时候 table 还没初始化,这个时候会执行 resize,进行table 数组的初始化,table 数组的初始容量保存在 threshold 中(如果从构造器中传入的一个初始容量的话),如果创建HashMap 的时候没有指定容量,那么 table 数组的初始容量是默认值:16。即,初始化 table 数组的时候会执行 resize 函数。

####### 2、扩容的时候会执行 resize 函数,插入元素后,当 size 的值 > threshold 的时候会触发扩容,即执行 resize 方法,这时 table 数组的大小会翻倍。

  • 注意我们每次扩容之后容量都是翻倍( * 2),所以HashMap的容量一定是2的整数次幂。
HashMap的容量为什么一定得是 2 的整数次幂呢?【面试重点】。

我们 put key 的时候,每一个 key 会对应到一个桶里面,桶的索引是这样计算的: index = hash & (n-1),index 的计算最为直观的想法是:hash % n,即通过取余的方式把当前的 key、value 键值对散列到各个桶中;那么这里为什么不用取余(%)的方式呢?

  • 原因是:
    ####### 1、* CPU对位运算支持较好,即位运算速度很快。另外,当 n 是 2 的整数次幂时:hash & (n - 1) 与 hash % n 是等价的,但是两者效率来讲是不同的,位运算的效率远高于取余 % 运算。****所以,HashMap中使用的是 hash & (n - 1)
    ####### 2、这还带来了一个好处,就是将旧数组中的 Node 迁移到扩容后的新数组中的时候有一个很方便的特性:【索引为 i 的节点,rehash后的索引只可能是 i 或者 i+oldcap,也就是我们可以这样处理:把 table[i] 这个桶中的 node 拆分为两个链表 l1 和 l2:如果hash & n == 0,那么当前这个 node 被连接到 l1 链表;否则连接到 l2 链表。这样下来,当遍历完 table[i] 处的所有 node 的时候,我们得到两个链表 l1 和 l2,这时我们令 newtab[i] = l1,newtab[i + n] = l2,这就完成了 table[i] 位置所有 node 的迁移(rehash),这也是 HashMap 中容量一定的是2的整数次幂带来的方便之处。 (因为Java8 是尾插,如果你一个一个的来放置的话,那么每个位置你都要遍历到该位置链表的尾部才能插入,耗时长【自己理解的】)】
  • 把旧数组数据移动到新数组,我们在扩充HashMap的时候,因为n变为 2 倍,n-1的mask范围在高位多1bit,所以只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,(看(e.hash & oldCap) == 0还是不等于 0 )
  • 元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:

    在这里插入图片描述

final Node<K,V>[] resize() {
	// 保留扩容前数组引用
	Node<K,V>[] oldTab = table;
	int oldCap = (oldTab == null) ? 0 : oldTab.length;
	int oldThr = threshold;
	int newCap, newThr = 0;
	if (oldCap > 0) {
		if (oldCap >= MAXIMUM_CAPACITY) {
			threshold = Integer.MAX_VALUE;
			return oldTab;
		}
		// 正常扩容:newCap = oldCap << 1
		else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
			// 容量翻倍,扩容后的threshold自然也是*2
			newThr = oldThr << 1; 
	}
	else if (oldThr > 0) 
	   newCap = oldThr;
	else {
	   // table数组初始化的时候会进入到这里
	   // 默认容量newCap = DEFAULT_INITIAL_CAPACITY;
		newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
	}
	if (newThr == 0) {
		float ft = (float)newCap*loadFactor;
		newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
	}
	threshold = newThr;    // 更新threshold
	@SuppressWarnings({"rawtypes", "unchecked"})
	// 扩容后的新数组
	Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
	table = newTab;        // 执行容量翻倍的新数组
	if (oldTab != null) {
		// 之后完成oldTab中Node迁移到table中去
		for (int j = 0; j < oldCap; ++j) {
			Node<K,V> e;
			if ((e = oldTab[j]) != null) {
				oldTab[j] = null;
				if (e.next == null)
				// j这个桶位置只有一个元素,直接rehash到table数组
					newTab[e.hash & (newCap - 1)] = e;
				else if (e instanceof TreeNode)
					// 如果是红黑树:也是将红黑树拆分为两个链表,这里主要看链表的拆分,两者逻辑一样
					((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
				else { 
					// 链表的拆分第一个链表l1
					Node<K,V> loHead = null, loTail = null;
 
					// 第二个链表l2
					Node<K,V> hiHead = null, hiTail = null;
					Node<K,V> next;
					do {
						next = e.next;
						if ((e.hash & oldCap)== 0) {
							// rehash到table[j]位置将当前node连接到l1上  
							if (loTail == null)
								loHead = e;
							else
								loTail.next = e;
							loTail = e;
						}
						else {
							// 将当前node连接到l2上
							if (hiTail == null)
								hiHead = e;
							else
								hiTail.next = e;
							hiTail = e;
						}
					} while ((e = next) != null);
 
					if (loTail != null) {
						// l1放到table[j]位置
						loTail.next = null;
						newTab[j] = loHead;
					}
					if (hiTail != null) {
						// l1放到table[j+oldCap]位置
						hiTail.next = null;
						newTab[j + oldCap] = hiHead;
					}
				}
			}
		}
	}
	return newTab;
} 
1.4 get
  • get 函数实质就是进行链表或者红黑树遍历搜索指定的 key 的节点的过程。

    • 另外需要注意到 HashMap 的 get 函数的返回值不能判断一个 key 是否包含在 map 中,get 返回 null 有可能是不包含该 key;也有可能该 key 对应的 value 为 null。因为 HashMap 中允许 key 为 null,也允许 value 为 null。
1) 第一个节点就匹配到了,直接返回,否则进行搜索
2) 判断是否是红黑树,是,就找红黑树
3)说明是链表,遍历链表来找
  • getNode的主要逻辑:如果 table[index] 处节点的 key 就是要找的 key 则直接返回该节点; 否则:如果在 table[index] 位置进行搜索,搜索是否存在目标 key 的 Node:这里的搜索又分两种:链表搜索和红黑树搜索,具体红黑树的查找就不展开了,红黑树是一种弱平衡(相对于AVL)BST,红黑树查找、删除、插入等操作都能够保证在O(logn)时间复杂度内完成,红黑树原理不在本文范围内,但是大家要知道红黑树的各种操作是可以实现的,简单点可以把红黑树理解为BST,BST的查找、插入、删除等操作的实现在之前的文章中有BST java实现讲解,红黑树实际上就是一种平衡的BST。
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) {
        // 判断第一个节点是不是就是需要的
        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; // 没找到
}

####### 6、contains 函数源码分析

public boolean containsKey(Object key) {
	// 注意与get函数区分,我们往map中put的所有的<key,value>都被封装在Node中,
	// 如果Node都不存在显然一定不包含对应的key
	return getNode(hash(key), key) != null;
}
二、Java8-ConCurrentHashMap
2.1 数据结构【结构上和 Java8 的 HashMap 基本上一样,不过它要保证线程安全性,所以在源码上确实要复杂一些。也引入了红黑树】

2.2 初始化【如果你有提供长度initialCapacity,那么数组长度为sizeCtl = 【 (1.5 * initialCapacity + 1),然后向上取最近的 2 的 n 次方】,但一般我们使用的默认构造器,不指定长度,默认数组长度是16】
// 这构造函数里,什么都不干
public ConcurrentHashMap() {
}
//通过提供初始容量,计算了 sizeCtl,sizeCtl = 【 (1.5 * initialCapacity + 1),然后向上取最近的 2 的 n 次方】。如 initialCapacity 为 10,那么得到 sizeCtl 为 16,如果 initialCapacity 为 11,得到 sizeCtl 为 32。
public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}
2.3 put【key和value都不能为null】
1)如果数组"空",进行数组初始化
2)找该 hash 值对应的数组下标【i = (n - 1) & hash)】,得到第一个节点 f
3)if: 数组该位置f为空,用一次 CAS 操作将这个新值放入其中即可,如果 CAS 失败,那就是有并发操作,进到下一个循环就好了(1,2,3,4,5步骤都在for循环里,直到我们某一步成功放入,才break)
4)else if: 容量不够,扩容,扩容后的数组容量为原来的两倍
5)else: 数组该位置f不为空,获取数组该位置的头结点的监视器锁synchronized (f)【对数组的每个位置操作时都会这样加锁头结点】 ,如果是treeNode,则用红黑树的方法插入,如果是链表,判断是否有重复,重复覆盖,不重复则加到链表末尾,如果链表长度大于8,与HashMap不同的是它不一定会转换为红黑树,比如当前数组长度如果小于64,那会选择进行数组扩容,而不是转换为红黑树
public V put(K key, V value) {
    return putVal(key, value, false);
}


final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    // 得到 hash 值
    int hash = spread(key.hashCode());
    // 用于记录相应链表的长度
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 1、如果数组"空",进行数组初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // 2、找该 hash 值对应的数组下标【i = (n - 1) & hash)】,得到第一个节点 f
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 3、如果数组该位置f为空,用一次 CAS 操作将这个新值放入其中即可,如果 CAS 失败,那就是有并发操作,进到下一个循环就好了
            if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
                break;    
        }
        // 4、【容量不够,扩容】hash 居然可以等于 MOVED,这个需要到后面才能看明白,不过从名字上也能猜到,肯定是因为在扩容
        else if ((fh = f.hash) == MOVED)
            // 帮助数据迁移,这个等到看完数据迁移部分的介绍后,再理解这个就很简单了
            tab = helpTransfer(tab, f);
 
        else { // 5、数组该位置f不为空,获取数组该位置的头结点的监视器锁synchronized (f) ,如果treeNode,则用红黑树的方法插入,如果是链表,判断是否有重复,重复覆盖,不重复则加到链表末尾,如果链表长度大于8,与HashMap不同的是它不一定会转换为红黑树,比如当前数组长度如果小于64,那会选择进行数组扩容,而不是转换为红黑树
            V oldVal = null;
            // 获取数组该位置的头结点的监视器锁
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                // 头结点的 hash 值大于 0,说明是链表
                    if (fh >= 0) { 
                        // 用于累加,记录链表的长度
                        binCount = 1;
                        // 遍历链表
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
   // 如果发现了"相等"的 key,判断是否要进行值覆盖,然后也就可以 break 了
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
            // 到了链表的最末端,将这个新值放到链表的最后面
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key, value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) { // 红黑树
                        Node<K,V> p;
                        binCount = 2;
                        // 调用红黑树的插值方法插入新节点
                        if ((p =((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            // binCount != 0 说明上面在做链表操作
            if (binCount != 0) {
                // 判断是否要将链表转换为红黑树,临界值和 HashMap 一样,也是 8
                if (binCount >= TREEIFY_THRESHOLD)
                    // 这个方法和 HashMap 中稍微有一点点不同,那就是它不是一定会进行红黑树转换,
                    // 如果当前数组的长度小于 64,那么会选择进行数组扩容,而不是转换为红黑树
                    //    具体源码我们就不看了,扩容部分后面说
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 
    addCount(1L, binCount);
    return null;
}


2.5 初始化数组:initTable
1) sc = sizeCtl,然后CAS将 sizeCtl 设置为 -1,代表抢到了锁,如果本身就是-1了,说明初始化这件事被其他线程抢去了,我就只需Thread.yield();就好
2)如果由我抢到了锁,初始化数组,长度为 16(默认长度) 或初始化时根据你提供的长度计算出来的长度,负载因子为0.75,table 是 volatile 的
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        // 初始化的"功劳"被其他线程"抢去"了
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        // CAS 一下,将 sizeCtl 设置为 -1,代表抢到了锁
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    // DEFAULT_CAPACITY 默认初始容量是 16
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    // 初始化数组,长度为 16 或初始化时提供的长度
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    // 将这个数组赋值给 table,table 是 volatile 的
                    table = tab = nt;
                    // 如果 n 为 16 的话,那么这里 sc = 12
                    // 其实就是 0.75 * n
                    sc = n - (n >>> 2);
                }
            } finally {
                // 设置 sizeCtl 为 sc,我们就当是 12 吧
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}
2.6 get
1)计算 hash 值
2)根据 hash 值找到数组对应位置: (n – 1) & h
3)根据该位置处结点性质进行相应查找【如果该位置为 null,那么直接返回 null ->如果该位置处的节点刚好就是我们需要的,返回该节点的值即可->如果该位置节点的 hash 值小于 0,说明正在扩容,或者是红黑树,后面我们再介绍 find 方法->如果以上 3 条都不满足,那就是链表,进行遍历比对即可】
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        // 判断头结点是否就是我们需要的节点
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        // 如果头结点的 hash 小于 0,说明 正在扩容,或者该位置是红黑树
        else if (eh < 0)
            // 参考 ForwardingNode.find(int h, Object k) 和 TreeBin.find(int h, Object k)
            return (p = e.find(h, key)) != null ? p.val : null;
 
        // 遍历链表
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值