JUC---ThreadLocal原理详解

news/2024/11/16 16:25:04/文章来源:https://www.cnblogs.com/Elmer1/p/18549440

什么是ThreadLocal?

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢?

JDK 中自带的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get()set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题

ThreadLocal 原理了解吗?

最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThreadLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。

每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。

ThreadLocal类中有静态内部类ThreadLocalMap,在ThreadLocalMap类中也有静态内部类Entry,而这个Entry类继承自WeakReference

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

比如我们在同一个线程中声明了两个 ThreadLocal 对象的话, Thread内部都是使用仅有的那个ThreadLocalMap 存放数据的,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。

ThreadLocal 数据结构如下图所示:

在每条线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是每条线程用来存储变量副本的,key值为当前ThreadLocal对象,value为变量副本(即T类型的变量)。每个Thread线程对象最开始的threadLocals都为空,当线程调用ThreadLocal.set()或ThreadLocal.get()方法时(get方法待会而会分析到),都会调用createMap()方法对threadLocals进行初始化。然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

ThreadLocalMap

ThreadLocal的原理是涉及三个核心类:ThreadLocalThread以及ThreadLocalMap类。在Thread类中存在两个成员变量:threadLocalsinheritableThreadLocals,这两个成员变量的类型都为ThreadLocalMap,经过一系列分析后我们可以得知,这两个成员变量是存储线程变量副本的最终容器,而前面也曾提到过:ThreadLocalMapThreadLocal中定制版的HashMap,但是它并没有实现Map接口,而是自己内部通过数组类型存储Entry实现。而Entry只是简单的继承了WeakReference弱引用,并没有没有实现类似HashMapNode.next的后继节点指向,所以ThreadLocalMap并不是链表形式的实现。哪没有了链表结构之后,ThreadLocalMap是如何解决哈希冲突的呢?

ThreadLocalMap是如何解决哈希冲突的呢? ---开放寻址法

在调用createMap()方法创建ThreadLocalMap示例时,在ThreadLocalMap的构造方法中,会为成员变量table初始化一个长度为16的Entry数组,通过hashCodelength位运算确定出一个下标索引值i,这个i就是被存储在table数组中的下标位置。

每条线程的threadlocals都会在内部维护独立table数组,而每个ThreadLocal对象在不同的线程table中位置都是相同的。对于同一条线程而言,不同的ThreadLocal变量副本都会被封装成一个个的Entry对象存储在自己内部的table中。

ok~,接着往下说,经过int i = key.threadLocalHashCode & (len-1);计算出索引下标值之后,会开始遍历table,然后会开始判断,如果table[i]位置不为空,但是原本的key值和现在新的key值是相同的情况下,则使用现在的新值替换掉之前的老值,刷新value值并返回;如果table[i]位置为空,则创建一个的Entry对象封装K-V值并将该对象放在table[i]位置;如果table[i]位置不为空并且Key不相同时,哪就调用nextIndex(i,len)获取下一个位置信息并判断下一个位置是否为空,直到找到为空的位置为止;在table[i]位置不为空并且Key不相同的情况下,如果遍历完整个table数组也没有找到为空的下标位置时,代表数组已经存满了需要扩容,则调用rehash()对数组扩容两倍

整个ThreadLocalMap存储过程结束,如下:

在get时,也会根据ThreadLocal对象的哈希值跟table数组长度进行计算获取下标索引值i,然后判断该位置Entry对象的key值与get(key)的key是否相同,如果相同则直接获取该位置的值并返回。如果不相同则遍历整个数组中table[i]之后的所有元素,循环判断下一个位置的key是否与传入进来的key一致,如果一致则获取返回

ThreadLocal 内存泄露问题是怎么导致的?

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。

这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后最好手动调用remove()方法

可不可以把value也变成弱引用?

不可以。因为存进ThreadLocal中正在使用的对象,在线程的栈中也有引用的,这是一根强引用指针,所以只要线程还在使用,就算内存不足,对应的Key也不会被回收;反之,如果key和value的关系都设计成弱引用,这时假设内存不足,触发GC就会导致value被回收,因为线程本身不直接持有value,而是通过key来间接性的访问value,如果value也是弱引用,就会出现“key还在,value因为内存不足,导致被GC回收”的问题

可不可以把key变成强引用?

不可以。既然key被设计成了弱引用,所以才会导致key=null的情况出现,那假设把key设计成强引用,是不是就解决了这个问题呢?先看个例子:

public static void main(String[] args) {  ThreadLocal TL = new ThreadLocal();  TL.set(new Object());  TL = null;  
}  

这里创建了一个ThreadLocal对象TL,并设置一个Object对象,然后将其置空。如果Key是强引用的话,TL无法被回收,也无法被访问,Object无法被回收,也无法被访问,Key和Value同时出现了内存泄漏

为啥K-V都内存泄漏了呢?因为最后一行置空代码,只能将main线程栈中的引用置空,而Thread对象内部有一个threadLocals成员,依旧会保持与ThreadLocalMap的引用,而Map的Key又强引用自ThreadLocal,这时main线程的栈,虽然没有引用这个TL,但Map却在引用着它,最终就导致了K-V都内存泄漏。

上面也是ThreadLocalMap中,为什么Key被设计成弱引用的原因,而且ThreadLocal也在尽可能的避免内存泄漏,当你调用set/get/remove()方法时,都会清理过期的Key(调用remove方法是最有效的)

综上所述:key设计成弱引用反而是最好的选择

ThreadLocalMap扩容机制了解吗?

  1. 触发扩容的条件
    • ThreadLocalMap初始容量是 16,它在存储元素时,当元素个数达到阈值(threshold)就会触发扩容。阈值的计算方式是数组容量(table.length)的三分之二
    • 例如,初始容量为 16 时,当存储的元素个数达到16 * 2/3 = 10(向下取整)个元素时,就会触发扩容。
  2. 扩容过程
    • 扩容是创建一个新的Entry数组,新数组的大小是原来的两倍
    • 然后遍历旧数组中的所有Entry,将其重新哈希(rehash)到新数组中。在重新哈希的过程中,会处理可能出现的哈希冲突。
    • 对于哈希冲突,ThreadLocalMap采用线性探测法来解决。即当发生冲突时,会顺序查找下一个可用的位置来存储元素。在扩容后的重新哈希过程中,这个线性探测的逻辑也会起作用。
    • 假设旧数组中有一个Entry在位置i,重新哈希时,它会先计算新的索引位置i' = i & (newLength - 1)(其中newLength是新数组的长度),如果这个位置没有被占用,就将Entry放入该位置;如果被占用了,就会线性探测下一个位置,直到找到一个空闲位置。

ThreadLocal怎么实现线程隔离的?

由于每个线程都有自己独立的ThreadLocalMap,所以不同线程之间的ThreadLocal变量是相互隔离的。即使多个线程使用了相同的ThreadLocal对象,它们所操作的也是各自线程中的变量副本,不会相互影响。

要说是怎么实现线程隔离的,其实就是在set()、get()方法的具体实现,我们set的值,为什么不会被其他的线程所读取。

Set()方法:

public void set(T value) {// 1、获取当前线程Thread t = Thread.currentThread();// 2、获取当前线程的threadlocals成员变量ThreadLocalMap map = getMap(t);// 3、判断map是否为nullif (map != null)// 如果不为null,就直接将value放进map中// key是当前的threadLocal,value就是传进来的值map.set(this, value);else// 如果为 null,初始化一个map,再将value 放进map中// key是当前的threadLocal,value就是传进来的值createMap(t, value);
}

Get()方法:

public T get() {// 获取到当前线程Thread t = Thread.currentThread();// 2、获取当前线程的threadlocals成员变量ThreadLocalMap map = getMap(t);//3、判断map是否为nullif (map != null) //3.1、如果不为null,根据当前的ThreadLocal 从当前线程中的ThreadLocals中取出map存储的变量副本ThreadLocalMap.Entry e = map.getEntry(this);// 如果存储的值不为null,就返回值if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//return setInitialValue();
}
  • 当在一个线程中调用ThreadLocalget()方法获取变量值时,它会首先获取当前线程的ThreadLocalMap,然后根据当前的ThreadLocal对象作为键,从ThreadLocalMap中查找对应的变量值并返回。如果找不到,则会返回null或根据初始化方法返回默认值。

这就是ThreadLocal的原理~~~❤️❤️❤️

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

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

相关文章

一文讲透 FPGA CDC 多bit跨时钟域同步-hand-shanking机制

一、背景 数据的跨时钟域处理是FPGA开发过程中的常见问题,存在两种情况慢时钟向快时钟同步:只需在快时钟域打两拍即可。其RTL如下:打拍同步的原理:大家在初学FPGA时,经常听过FPGA中对信号打拍可以有效得避免亚稳态,而且一般要打两拍,其数学本质是如果打一拍发生错误得概…

KBPC3510-ASEMI整流桥KBPC3510参数、封装、尺寸

KBPC3510-ASEMI整流桥KBPC3510参数、封装、尺寸编辑:ll KBPC3510-ASEMI整流桥KBPC3510参数、封装、尺寸 型号:KBPC3510 品牌:ASEMI 封装:KBPC-4 正向电流:35A 反向电压:1000V 引脚数量:4 芯片个数:4 芯片尺寸:50MIL 漏电流:>10ua 恢复时间:>2000ns 浪涌电流:…

IDEA中操作表

Navicat中创建的表,同时也可以在IDEA中打开。 IDEA中侧边可以创建架构 可以选择相应排序规则以及创建新的表,但不如navicat方便

使用while循环分别对两个vector进行赋值,该怎么做

问题 在写程序的时候遇到了这样一个问题,见代码 #include <iostream> #include <vector>using namespace std;bool isequal(vector<int> vshort, vector<int> vlong) {for (int index = 0; index != vshort.size(); index++)if (vshort[index] != vlo…

合并具有文本框的Word文档:VBA代码批量操作

本文介绍基于VBA语言,对大量含有图片、文本框与表格的Word文档加以批量自动合并,并在每一次合并时添加分页符的方法~本文介绍基于VBA语言,对大量含有图片、文本框与表格的Word文档加以批量自动合并,并在每一次合并时添加分页符的方法。在我们之前的文章中,介绍过基于Pytho…

streamstring类介绍

std::stringstream 是 C++ 标准库中提供的一个类,定义在头文件 <sstream> 中。它是基于字符串的流(stream),允许像操作输入流(std::cin)或输出流(std::cout)那样,操作字符串内容。 std::stringstream 是 std::iostream 的派生类,支持同时进行字符串解析(输入)…

爬虫

程序示例: import java.util.regex.Matcher; import java.util.regex.Pattern;public class RegexDemo6 {public static void main(String[] args) {/** 有如下文本, 请按照要求爬取数据. * Java 自从 95 年问世以来, 经历了很多版本, 目前企业中用的最多的是 Java8 和 Java11,…

将数值转换为字符串的函数

在 C++ 中,itoa 和 sprintf 是用于将数值转换为字符串的经典函数。然而,它们有一定的局限性或者安全性问题,现代 C++ 更倾向于使用标准库的解决方案,如 std::to_string 和 std::stringstream,来代替这些函数。 1. itoa 的替代 itoa 是一种将整数转换为字符串的函数,但它不…

20222406 2024-2025-1 《网络与系统攻防技术》实验五实验报告

20222406 2024-2025-1 《网络与系统攻防技术》实验五实验报告 1.实验内容对网站进行 DNS 域名查询,包括注册人、IP 地址等信息,还通过相关命令查询 IP 地址注册人及地理位置。尝试获取 QQ 好友 IP 地址并查询其地理位置。使用 nmap 对靶机环境扫描,获取靶机 IP 活跃状态、开…

delphi 新版内存表 FDMemTable

c++builder XE 官方demo最全60多个 http://community.embarcadero.com/blogs?view=entry&id=8761FireDAC.Comp.Client用好FDMemTable代替之前的ClientDataSet,以前ClientDataSet内存表转换太繁琐了步骤。TClientDataSet *cds = new TClientDataSet(this); DataSetProvid…

理想雪 - 翠鸟协会

写在前面3844 字 | 小说 | 热爱 | 思考 | 表达 | 坚定 | 证明 | 坚守《理想雪》系列故事均为架空世界观,所有人名、地名等与现实世界无任何关联。该系列只且仅只为了说明,小说作者在该情境下会诞生的想法和采取的行动,以及背后的世界观、价值观和人生观。因此将具有强烈的个…

专题课:综合案例5

评委打分解答: 1.首先肯定要键盘录入6个评委的分数6个评委的分数,即6个变量,我们肯定用数组更加方便,因为后面求和求最大值之类的,用数组都更简单 遍历数组,我们每键盘打出一个元素就将其放入数组中 . 2.然后定义求和变量,将6个分数求和3.for循环搭配if筛选求最大、最小…