首页 > 编程语言 >【collection】2.java容器之HashMap&LinkedHashMap&Hashtable2

【collection】2.java容器之HashMap&LinkedHashMap&Hashtable2

时间:2022-12-06 10:23:54浏览次数:43  
标签:Node java HashMap int Hashtable2 key tab hash null

ConcurrentHashMap

put操作

final V putVal(K key, V value, boolean onlyIfAbsent) {
	if (key == null || value == null) throw new NullPointerException();
	// 本质上和hashmap没有什么差别,都是把hashcode进行对半异或,这样就可以用一半的位数,集合了32位长度的信息
	int hash = spread(key.hashCode());
	int binCount = 0;
	Node<K,V>[] tab = table;
	// 这里循环的目的是cas的重试操作
	for (;;) {
		// 指向待插入元素应当插入的位置
		Node<K,V> f;
		// 元素f对应的哈希值
		int fh;
		// 当前hash表数组的长度容量
		int n;
		int i;
		// 如果哈希数组还未初始化,或者容量无效,则需要初始化一个哈希数组
		if (tab == null || (n = tab.length) == 0) {
			tab = initTable();
			// 这里n -1 & hash 是经典取余操作,参考之前hashmap
		} else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
			// 如果当前位置是空的,那么就可以通过cas设置对应的值
			Node<K, V> newNode = new Node<K,V>(hash, key, value, null);
			// 用一次 CAS 操作将这个新值放入其中即可,这个 put 操作差不多就结束了,可以拉到最后面了
			// 如果 CAS 失败,那就是有并发操作,进到下一个循环就好了
			if (casTabAt(tab, i, null, newNode)) {
				// 插入完成,跳出循环,如果更新失败,重新循环进入
				break;                   // no lock when adding to empty bin
			}
		} else if ((fh = f.hash) == MOVED) {
			/*
			 * 如果待插入元素所在的哈希槽上已经有别的结点存在,且该结点类型为MOVED
			 * 说明当前哈希数组正在扩容中,此时,可以尝试加速扩容过程
			 */
			tab = helpTransfer(tab, f);
		} else {
			V oldVal = null;
			// 这里避免并发,对f节点的引用进行上锁
			synchronized (f) {
				// 如果tab[i]==f,则代表当前待插入状态仍然可信
				if (tabAt(tab, i) == f) {
					// fh > 0 标识不是在扩容,是正常节点
					if (fh >= 0) {
						binCount = 1;
						// 遍历链表
						for (Node<K,V> e = f;; ++binCount) {
							K ek;
							// 找到对应的值
							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;
						}
					}
				}
			}
			// 如果对已有元素搜索过,则计数会发生变动,这里需要进一步观察
			if (binCount != 0) {
				// 哈希槽(链)上的元素数量增加到TREEIFY_THRESHOLD后,这些元素进入波动期,即将从链表转换为红黑树
				if (binCount >= TREEIFY_THRESHOLD) {
					// 注意,这里也只是锁了这一个节点,注意,这里不一定一定是转换为红黑树
					// 如果整个tab长度是小于64的话,这里会选择自动扩容,如果已经超过64了,才考虑转换红黑树
					treeifyBin(tab, i);
				}
				if (oldVal != null)
					return oldVal;
				break;
			}
		}
	}
	addCount(1L, binCount);
	return null;
}

get和remove操作

略,就是根据索引找节点

扩容

核心方法就是transfer

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
	int n = tab.length;
	// stride 在单核下直接等于 n,多核模式下为 (n>>>3)/NCPU,最小值是 16
	//   将这 n 个任务分为多个任务包,每个任务包有 stride 个任务
	int stride = (NCPU > 1) ? (n >>> 3) / NCPU : n;
	if (stride < MIN_TRANSFER_STRIDE) {
		stride = MIN_TRANSFER_STRIDE; // subdivide range
	}
	if (nextTab == null) {            // initiating
		try {
			// 直接扩大一倍
			@SuppressWarnings("unchecked")
			Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
			nextTab = nt;
		} catch (Throwable ex) {      // try to cope with OOME
			sizeCtl = Integer.MAX_VALUE;
			return;
		}
		nextTable = nextTab;
		transferIndex = n;
	}
	int nextn = nextTab.length;

	// ForwardingNode 翻译过来就是正在被迁移的 Node
	// 这个构造方法会生成一个Node,key、value 和 next 都为 null,关键是 hash 为 MOVED
	// 后面我们会看到,原数组中位置 i 处的节点完成迁移工作后,
	//    就会将位置 i 处设置为这个 ForwardingNode,用来告诉其他线程该位置已经处理过了
	//    所以它其实相当于是一个标志。, 这个在put的时候会判断节点hash值,用来判断是否需要协助扩容
	ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
	// advance 指的是做完了一个位置的迁移工作,可以准备做下一个位置的了
	boolean advance = true;
	boolean finishing = false; // to ensure sweep before committing nextTab
	// 循环所有的节点位置,判断是否需要进行迁移
	for (int i = 0, bound = 0;;) {
		Node<K,V> f; int fh;
		while (advance) {
			int nextIndex, nextBound;
			if (--i >= bound || finishing) {
				advance = false;
			} else if ((nextIndex = transferIndex) <= 0) {
				i = -1;
				advance = false;
			} else if (U.compareAndSwapInt
					 (this, TRANSFERINDEX, nextIndex,
					  nextBound = (nextIndex > stride ?
								   nextIndex - stride : 0))) {
				bound = nextBound;
				i = nextIndex - 1;
				advance = false;
			}
		}
		if (i < 0 || i >= n || i + n >= nextn) {
			int sc;
			if (finishing) {
				nextTable = null;
				table = nextTab;
				sizeCtl = (n << 1) - (n >>> 1);
				return;
			}
			if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
				if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
					return;
				finishing = advance = true;
				i = n; // recheck before commit
			}
		}
		else if ((f = tabAt(tab, i)) == null)
			advance = casTabAt(tab, i, null, fwd);
		else if ((fh = f.hash) == MOVED)
			advance = true; // already processed
		else {
			// 真正的开始数据迁移,先对f节点上锁,f是tab中的一个位置
			synchronized (f) {
				// 保证数据正确性没有发生变化
				if (tabAt(tab, i) == f) {
					Node<K,V> ln, hn;
					// 头节点的 hash 大于 0,说明是链表的 Node 节点
					if (fh >= 0) {
						// 下面这一块和 Java7 中的 ConcurrentHashMap 迁移是差不多的,
						// 需要将链表一分为二,
						// 找到原链表中的 lastRun,然后 lastRun 及其之后的节点是一起进行迁移的
						// lastRun 之前的节点需要进行克隆,然后分到两个链表中
						int runBit = fh & n;
						Node<K,V> lastRun = f;
						// 循环链表,获取到尾节点
						for (Node<K,V> p = f.next; p != null; p = p.next) {
							int b = p.hash & n;
							if (b != runBit) {
								runBit = b;
								lastRun = p;
							}
						}
						if (runBit == 0) {
							ln = lastRun;
							hn = null;
						} else {
							hn = lastRun;
							ln = null;
						}
						// 从头循环到尾部
						for (Node<K,V> p = f; p != lastRun; p = p.next) {
							int ph = p.hash;
							K pk = p.key;
							V pv = p.val;
							// ph是这个节点的hash值&n如果为0,说明再n-1部分,位置没变,还是放入之前的位置
							if ((ph & n) == 0) {
								ln = new Node<K,V>(ph, pk, pv, ln);
							} else {
								// 不为0,说明再n-1上面的位置,位置变了,那么就重新设置位置
								hn = new Node<K,V>(ph, pk, pv, hn);
							}
						}
						// 其中的一个链表放在新数组的位置 i
						setTabAt(nextTab, i, ln);
						// 把head放到数组i+n的位置
						setTabAt(nextTab, i + n, hn);
						// 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
						//    其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
						setTabAt(tab, i, fwd);
						advance = true;
					} else if (f instanceof TreeBin) {
						// 红黑树的迁移
						TreeBin<K,V> t = (TreeBin<K,V>)f;
						TreeNode<K,V> lo = null, loTail = null;
						TreeNode<K,V> hi = null, hiTail = null;
						int lc = 0, hc = 0;
						for (Node<K,V> e = t.first; e != null; e = e.next) {
							int h = e.hash;
							TreeNode<K,V> p = new TreeNode<K,V>
								(h, e.key, e.val, null, null);
							if ((h & n) == 0) {
								if ((p.prev = loTail) == null)
									lo = p;
								else
									loTail.next = p;
								loTail = p;
								++lc;
							}
							else {
								if ((p.prev = hiTail) == null)
									hi = p;
								else
									hiTail.next = p;
								hiTail = p;
								++hc;
							}
						}
						ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
							(hc != 0) ? new TreeBin<K,V>(lo) : t;
						hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
							(lc != 0) ? new TreeBin<K,V>(hi) : t;
						setTabAt(nextTab, i, ln);
						setTabAt(nextTab, i + n, hn);
						setTabAt(tab, i, fwd);
						advance = true;
					}
				}
			}
		}
	}
}

标签:Node,java,HashMap,int,Hashtable2,key,tab,hash,null
From: https://www.cnblogs.com/cutter-point/p/16954443.html

相关文章

  • (转)java爬虫 httpclient htmlunit selenium 比较
    原文链接:https://blog.csdn.net/qq_34661726/article/details/80585659简单介绍。1httpclienthttpclient是HttpClient是ApacheJakartaCommon下的子项目,支持常......
  • java抽象类的定义和使用
    1.抽象类的规则●抽象类不可以被实例化,也就是不能被new,会出现编译错误。抽象类如果想实例化可以通过非抽象子类的方式去实现。●抽象类中不一定有抽象方法,但有抽象方......
  • Java常见面试题
    1. Java中sleep和wait的区别①这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类。sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用b的sle......
  • Java程序设计——从方法学角度描述
    Java程序设计——从方法学角度描述作者:化志章 揭安全 钟林辉出版社:机械工业出版社  一、程序设计语言概述1.1程序的含义和程序设计策略1.2程序设计语言的......
  • Java使用LinkedList模拟一个堆栈或者队列数据结构
    用Java模拟一个堆栈或者队列数据结构。首先得明白堆栈和队列的数据结构:堆栈:先进后出队列:先进先出LinkedList中刚好有addFirst()和addLast()方法。1.publicclassStac......
  • java 如何正确使用接口返回对象Result
    1.Result的使用Result的使用,是java项目中开发接口的必备,它经常被我们用作接口的返回对象,方便前端或者其他程序的远程调用后处理业务。它一般包括以下几个属性:code:一般......
  • java 获取真实ip
    通过HttpServletRequest获取真实请求IPpackagecc.library.security.utils;importjavax.servlet.http.HttpServletRequest;/***CREATEBYfunnyZpCON2018/5/3......
  • Java网络编程---基于TCP协议实现客户端服务端通信
    首先,对于TCP协议,我们要明确:TCP:传输控制协议TCP会尽自己所能,尽量将数据发送给对方;但并不能保证100%可以发送给对方TCP会在数据发送不到对方的情况下,会给应用......
  • JavaSE复习day1
    JavaSE复习day1胡家伟1.代码格式注释单行注释:通常用于解释方法内某单行代码的作用。多行注释:通常用于解释一段代码的作用。文档注释:通常用于生成Java开发文档。......
  • 用Java实现分布式缓存(1)——缓存淘汰
    本文代码https://github.com/weloe/Java-Distributed-Cache/tree/main/src/main/java/com/weloe/cache/outstrategyhttps://github.com/weloe/Java-Distributed-Cache/tr......