如有转载请注明出处
https://blog.youkuaiyun.com/weixin_41955327/article/details/90228701public V put(K key, V value) { //点击进入putVal return putVal(key, value, false); } /** Implementation for put and putIfAbsent */ final V putVal(K key, V value, boolean onlyIfAbsent) { //检查传入的参数是否规范, 注意这里的key 不同于HashMap 中可存储null 的key if (key == null || value == null) throw new NullPointerException(); // 计算key 的hash 值 用来后面定位元素 int hash = spread(key.hashCode()); int binCount = 0; // table 引用指向的是ConcurrentHashMap中 所有元素所存在的数组的引用 所以下面依次将遍历 for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; //如果tab是null 的那么在放入元素前先初始化,如何初始化,在下面解释 if (tab == null || (n = tab.length) == 0) tab = initTable(); // 如果是初始化完毕 那么定位当前要put的元素应该放在table 中哪个下表位置上,使用方法tabAt 得到下标位置并且当前定位的下标 ///位置上没有元素存储即为空,专业说当前下标没有发生hash冲突 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // casTabAt 方法是使用cas 将元素put 进去,put成功后break 循环 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } // 因为多线程操作下,可能其他线程正在进行此位置的节点链表的扩容,那么就帮助扩容 //多补充一下,ConcurrentHashMap 是可以多线程并发扩容,那么避免并发操作互相影响,有一个步长的概念,例如 我们的数据机构为20长度的数据,如果同时有四个线程并发,那么数组中平均每五个用一个线程来帮助他扩容,第一个线程可能区扩容数组中下表为0的,但是此线程在扩容下一个数组中的位置应该是下标为4的位置,刚好步长为5(依次0 4 8 12 16 20), 依次类推。第二个线程扩容 2 6 10 14 18..... else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null; //开始锁住这个定位到的数组下标位置上的元素,开始在这个链表上插入 synchronized (f) { //保持健壮性,再一次计算 数组中下表的位置,是否和上面f的位置相同 if (tabAt(tab, i) == f) { //fh 在上边条件判断时就已经赋值了 不要忘记,“ else if ((fh = f.hash) == MOVED)” 大于0的hash值证明是链表 if (fh >= 0) { binCount = 1; //遍历这个链表 每遍历一次binCount 会+1 操作,不要怀疑+1操作的原子性,因我们现在在锁内“ synchronized (f) {” for (Node<K,V> e = f;; ++binCount) { K ek; //要记住发生hash冲突才再数组上挂链表去解决这种问题,那么我们在put的时候就要知道,在这个链表上是挂着的每个元素(Node)是不是重复的在put key,所以在放入链表上的时候要一一遍历当前链表,如果真的重复就把value 插入 在这个条件判断上要知道一件事,如果key 的hash 值相同不能代表key 一定相同,但是key 的hash值不同一定代表key 不同,这个您百度一下hash 的定义很容易明白,所以hash 值比较后还需用equals 或者 == 号 来判断是不是真的为同一个key 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; //在这个条件判断中赋值e 为链表的下个元素,同时还判断是否为null 如果,为null 就说明已经是遍历到链表的最后一个位置上了 在这里还要说下,jdk 源码写的非常精炼,赋值和条件判断往往是写在一起的,让人看起来容易发蒙。。。。。 if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } //如果 定位的当前的数组中元素 f 数红黑树,那么就按照红黑树结构来插入节点 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; } } } } //要知道,当插入链表后值大于8 的时候节点应当 转红黑树局结构 if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } //用来对加入的元素+1 为size 服务 addCount(1L, binCount); return null; }
// 解释如何初始化
//之所以在put时候初始化是因为在构造时没有初始化,只有真正用put的时候才会去初始化
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
//首先要知道在ConcurrentHashMap 中sizeCtl 这个常量值所代表的状态, 当sizeCtl =-1 时表示正在初始化 -n 当前有n-1个线程正在进行扩容, 0 表示当前的table 还没有被初始化,大于0表示初始化的大小而且也是一个阈值达到这个数量后需要扩容
if ((sc = sizeCtl) < 0)
//因为有个线程在扩容所以,我们当前的线程要礼让cpu
Thread.yield(); // lost initialization race; just spin
//这是一个cas 操作将当前的值赋值为-1 让其他线程知道有个线程正在扩容这个数组。
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// 用数学角度来看 1 - 1/4 =0.75 即当数组的大小达到当前的75% 会再次扩容
sc = n - (n >>> 2);
}
} finally {
// 将当前扩后的的值赋给sizeCtl 让其作为下次扩容的阈值
sizeCtl = sc;
}
break;
}
}
return tab;
}
- 感谢您的阅读。如果感觉文章对您有用,麻烦您动动手指点个赞,以资鼓励。谢谢!
如有转载请注明出处
https://blog.youkuaiyun.com/weixin_41955327/article/details/90228701