ThreadLocalMap为什么用线性探测解决哈希冲突

前言

ThreadLocal 本身不存储值,访问的是当前线程 ThreadLocalMap 里存储的数据副本,实现了线程间的数据隔离。只有当前线程能访问,也就不存在并发访问时的安全问题了。
ThreadLocal 的核心是 ThreadLocalMap,它和 HashMap 不同的是:面对哈希冲突时,后者用的是链表法,而前者用的是线性探测法,为什么呢???

理解线性探测

哈希表的实现,一般是创建一个数组,然后根据 key 计算哈希值,再根据数组长度计算一个下标,把 key value 封装成一个 Entry 写入当前下标位置即可。一旦发生哈希冲突,问题就来了,一块地址不能同时存储两个元素啊,这时就出现了几种常见的哈希冲突的解决方案。

链表法是 HashMap 的解决方案,把哈希冲突的 Entry 构建成链表即可,查找时得遍历整个链表。
线性探测是 ThreadLocalMap 的解决方案,它的思路是:一旦发生哈希冲突,就继续往后找(环形),找到第一个空节点的位置,再把当前 Entry 放进去。查找的过程也是一样的,先根据哈希值计算下标,再从这个位置开始往后找,如果找到第一个空节点还没找到,就认为 key 不存在。
所以,使用线性探测法有一个前提,数组必须能容纳所有的元素,否则就会出现死循环。一般情况下,使用线性探测法的哈希表,每次放入一个 Entry 后都要判断是否要扩容,确保有足够的容量存储下一个 Entry。

如下图所示,元素8、9分别占用了下标2、3的位置,此时元素14要放进来,下标计算也是2,但是因为已经有元素8了,所以只能往后继续找,直到发现下标4的地方是空的,元素就可以放进去了。
image.png

ThreadLocalMap

要知道为啥 ThreadLocalMap 用线性探测法,必然和它的某些特性相关,那就深入源码一探究竟。

ThreadLocalMap 虽然叫Map,但是并没有实现Map接口,只提供了简单的get、set方法。内部节点类 Entry 对 Key Value 进行了封装,且继承自 WeakReference,也就是说 Entry 对 ThreadLocal 是弱引用,这主要是为了清理过期节点,避免内存泄漏。

static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}
}

ThreadLocal#set 其实是把 ThreadLocal 作为 key,参数作为 value 写入到 ThreadLocalMap,方法是ThreadLocalMap#set 。在set新值时,有几种情况:

  • 如果发现key已经存在,直接替换value即可
  • 否则线性探测向后查找,如果发现了过期节点,替换过期节点。
  • 如果以上都没发生,那么肯定会找到一个空节点,直接插入即可,插入后判断是否扩容
private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();if (k == key) {// key相同,直接替换Valuee.value = value;return;}if (k == null) {// 过程中找到过期节点,有两种情况要处理:// 1. key不存在,直接替换掉过期节点即可,线性探测也不会中断// 2. key存在(在后面),接着往后找,把目标节点替换到当前过期节点replaceStaleEntry(key, value, i);return;}}// 找到了空节点,插入tab[i] = new Entry(key, value);int sz = ++size;// 因为插入一个新节点,所以要判断是否需要扩容// 如果尝可以清理掉部分过期节点,那就无需扩容// 如果数量没有超过阈值,也无需扩容if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}

上述第2步要特别注意:并不能单纯的替换过期节点,因为相同的key可能在过期节点的后面,如果直接替换就会导致相同的key被插入两次,程序就出bug了,所以专门写了个replaceStaleEntry() 用来替换过期节点。
替换过期节点时,还捎带做了一些其他事:

  • 从当前过期节点往前找,看看能否扫描到一些过期节点,捎带清理一下
  • 从当前过期节点往后找,如果发现相同key,就替换指定节点。找不到相同key,就替换过期节点
  • 最后把整个过程中扫描到的过期节点做清理
private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {Entry[] tab = table;int len = tab.length;Entry e;// 往前查找最久的过期节点,稍后清理掉int slotToExpunge = staleSlot;for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len))if (e.get() == null)slotToExpunge = i;// 线性探测往后找,找到key就替换,找不到就直接插入到当前过期节点for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();// 找到了keyif (k == key) {// 替换valuee.value = value;// 替换节点tab[i] = tab[staleSlot];tab[staleSlot] = e;if (slotToExpunge == staleSlot)// 前面没有过期节点,从当前节点开始往后清理&rehashslotToExpunge = i;// 从slotToExpunge开始往后清理&rehashcleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}// 往前没找到过期节点,但是往后找到了,那就从当前位置开始清理if (k == null && slotToExpunge == staleSlot)slotToExpunge = i;}// key没找到,直接写入当前过期节点tab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);// 往前/往后扫描到了过期节点,清理它if (slotToExpunge != staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

清理过期节点并不容易,不是把它从数组里移除那么简单。因为线性探测的原因,如果过期节点后面还有节点的话,单纯的把过期节点移除掉,会导致整个探测的链路断掉,程序就出bug了。
expungeStaleEntry() 用来清理过期节点,主要做了两件事:

  • 将当前过期节点设为null
  • 向后查找,扫描到过期节点就清理掉,正常节点就rehash操作,重新放进哈希表,以此来保证探测的链路完整
private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// 删除过期节点,本质是置为nulltab[staleSlot].value = null;tab[staleSlot] = null;size--;// rehash后续不为null的节点,不然会中断线性探测Entry e;int i;for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();if (k == null) { // 发现过期节点,顺带清理掉e.value = null;tab[i] = null;size--;} else {// 重新计算下标,如果不一样就rehashint h = k.threadLocalHashCode & (len - 1);if (h != i) {tab[i] = null;// rehash:线性探测,直到发现一个空节点插入while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i;
}

尾巴

回到问题本身,为什么 ThreadLocalMap 用线性探测来解决哈希冲突而不是链表法?本质上它和 HashMap 的定位是不一样的,HashMap 是性能优先,尽可能的保证元素的高效访问。ThreadLocalMap 性能不是第一要素,看完源码你会发现,如果数组元素比较密集的话,ThreadLocalMap 不管是 set 还是 get 都会不可避免地扫描很多节点,这肯定会影响性能。但是换来的收益,就是 ThreadLocalMap 可以在扫描节点时主动发现过期节点且清理掉,尽可能的避免内存泄漏,这比牺牲一点性能更加值得。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/414818.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【数据结构与算法】quicksort快速排序算法大全:快速排序hoare法,快速排序挖坑法,快速排序前后指针法,快速排序优化,快速排序的非递归实现

一、快速排序算法 快速排序使用分治的思想来进行排序&#xff0c;其基本过程如下&#xff1a; 从待排序数组中选择一个元素作为枢轴&#xff08;pivot&#xff09;。将数组分割成两个子数组&#xff0c;使得左侧子数组的元素都小于等于枢轴&#xff0c;右侧子数组的元素都大于…

【C++初阶】第二站:类与对象(上) -- 上部分

前言: C学习的第二站&#xff1a;类和对象(上)文章的上半部分,知识点:面向过程和面向对象初步认识、类的引入、类的定义、类的访问限定符及封装、类的作用域、类的实例化. 目录 面向过程和面向对象初步认识 类的引入 类的定义 类的访问限定符及封装 访问限定符 封装 类的…

【Docker】安装nacos以及实现负载均衡

&#x1f973;&#x1f973;Welcome 的Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于Docker的相关操作吧 目录 &#x1f973;&#x1f973;Welcome 的Huihuis Code World ! !&#x1f973;&#x1f973; 前言 一.nacos单个部署 1.镜像拉取 …

【Linux】Linux系统编程——pwd命令

文章目录 1.命令概述2.命令格式3.常用选项4.相关描述5.参考示例 1.命令概述 pwd&#xff08;Print Working Directory&#xff09;命令用于显示用户当前工作目录的完整路径。这是一个常用的命令&#xff0c;帮助用户确定他们目前所在的目录位置。 2.命令格式 基本的 pwd 命令…

Excel表格的快速动态扩展与删除行

实例需求&#xff1a;工作表中的表格&#xff08;ListObject&#xff09;名称为Table1&#xff0c;表格列数不确定&#xff0c;需要实现如下功能&#xff1a; 当用户完成最后一行最后一列输入之后&#xff08;如果该单元格为空&#xff0c;则视为输入未完成&#xff09;&#…

Python 生成 文件目录网页 下载超链接和打开 笔记

1. 一键生成 文件目录网页 下载超链接 1.1 图&#xff1a; 1.2 代码&#xff1a; &#xff08;由Ai生成部分&#xff0c;再改成适合自己用&#xff09; index.py # -*- coding: utf-8 -*- import os # import syspath E:\BIT\public\software\\ # path path os.getcwd() …

Java基础知识整理,驼峰规则、流程控制、自增自减

写在开头 本文接着上一篇文章续写哈。Java基础知识整理&#xff0c;注释、关键字、运算符 在这一篇文章中我们总结了包括注释、关键字、运算符的Java基础知识点&#xff0c;今天继续来聊一聊命名规则&#xff08;驼峰&#xff09;、流程控制、自增自减。 一、命名规则 上一…

书生·浦语 LMDeploy 大模型量化部署原理

文章目录 大模型部署背景模型部署大模型的特点大模型部署的挑战大模型部署方案 LMDeploy简介 大模型部署背景 模型部署 将训练好的模型在特定软硬件环境中启动的过程&#xff0c;使模型能够接收输入并返回预测结果为了满足性能和效率的需求&#xff0c;常常需要对模型进行优化…

SpringBoot项目打包

1.在pom.xml中加入如下配置 <build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-assembly-plugin</artifactId><version>3.1.0</version><configuration><descriptorRef…

CentOS stream 9配置网卡

CentOS stream9的网卡和centos 7的配置路径&#xff1a;/etc/sysconfig/network-scripts/ifcfg-ens32不一样。 CentOS stream 9的网卡路径&#xff1a; /etc/NetworkManager/system-connections/ens32.nmconnection 方法一&#xff1a; [connection] idens32 uuid426b60a4-4…

CTF CRYPTO 密码学-2

题目名称&#xff1a;crypto1 题目描述&#xff1a; 字符 ZZZZ X XXZ ZZ ZXZ Z ZXZ ZX ZZX XXX XZXX XXZ ZX ZXZZ ZZXZ XX ZX ZZ 分析 此字段是由Z和X组成的字符&#xff0c;联想到莫斯密码是由.和-组成的所以接下来可以尝试莫斯密码解题 解题过程&#xff1a; Step1&#xf…

双亲委派模型

其实分两步&#xff1a;检查&#xff08;自底而上&#xff09;—>加载&#xff08;自顶向下&#xff09; 看个例子&#xff1a;Class.forName(“com.mysql.cj.jdbc.Driver”); 但是&#xff01;&#xff01;&#xff01;在JDBC4.0之后就不需要这行代码了&#xff0c;使…