聊聊ThreadLocal(二)

news2025/1/12 16:09:42
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

大部分面试官喜欢问ThreadLocal,却错误地以为东西是存在ThreadLocal中,并且笃定key是当前线程...

其实Java的线程共享机制,最重要的是Thread中的ThreadLocalMap,ThreadLocal其实不重要,它只是一个钩子。东西实际被存在每一个Thread的ThreadLocalMap中,所以广义上可以理解为东西是存在Thread中。关于三者的关系,急性子的朋友可以直接先拉到文章末尾看看那张图。

ThreadLocal.set(T value)的key是Thread吗?

首先,要更新一下大家固有的认知:ThreadLocal其实不存东西,ThreadLocalMap的key也不是Thread。

很多人,包括很多面试官,都认为ThreadLocal在执行set(T value)时是把当前线程作为key存入自己内部的map中,大致相当于这样:

为什么他们会这样认为呢?大概是因为他们“看过”set(T value)的源码:

createMap(t, value),我去,这不就是传入一个Thread和value然后在内部构建一个Map吗?不用想了,ThreadLocal内部肯定有个Map,key就是Thread!

但真的是这样吗?

实际上,如果你点进createMap()会发现:

t.threadLocals其实是Thread内部的ThreadLocalMap,这里正在给Thread的ThreadLocalMap赋值呢,而且ThreadLocalMap的key是this,也就是当前ThreadLocal,而不是Thread。

是不是打破三观,甚至觉得有点懵?没关系,下面才是正文开始,会慢慢解释ThreadLocal的来龙去脉。

如何理解ThreadLocal是一个钩子?

如上图,Thread t1被实例化后,其实内部有个ThreadLocalMap,刚开始是null。然后t1.start()后线程就开始跑了(沿着箭头),当线程执行到

ThreadLocal tl1 = new ThreadLocal(); 
t1.set("你好");

时,ThreadLocal会作为一个钩子,尝试从Thread t1中钩出ThreadLocalMap。如果发现这个成员变量尚未赋值,则new ThreadLocalMap()并把map设置进去。特别注意,由于set()是ThreadLocal的方法,所以map.set(this, value)中的this显然是ThreadLocal tl1。

所以,ThreadLocalMap的key并不是Thread,而是ThreadLocal!!!

此时此刻,内存中有三个对象,Thread t1、ThreadLocal tl1、ThreadLocalMap map,其中Thread的成员变量map指向堆中新建的ThreadLocalMap。

你可以理解为:

ThreadLocal是紫霞仙子,而Thread是至尊宝,500年前在花果山的时候,紫霞一剑劈开至尊宝的胸膛(getMap),看看他有没有心(ThreadLocalMap)。此时发现至尊宝没有心,于是造了一颗心并且在心里留下一滴眼泪(紫霞:泪水)

对着上面的流程图,看看是不是这么回事。

调用threadLocal.get()到底发生了什么?

500年后,至尊宝走啊走,走到了盘丝洞,又遇到了紫霞仙子(ThreadLocal),紫霞再次劈开了至尊宝的胸膛(getMap),发现已经有心了,于是在至尊宝的心里找到名为“紫霞”的那滴泪水。

至此,大家已经明白同一个thread是如何在Controller存入值,然后在Service取出值的。

多个Thread与同一个ThreadLocal

上面讲的是一个Thread和一个ThreadLocal。接下来,我们探究一下多个Thread与同一个ThreadLocal:为什么访问同一个threadLocal.get(),Thread1存入的值不会被Thread2取出来?

其实很简单,你想想,同一个ThreadLocal表示从始至终只有一个紫霞仙子,而Thread1和Thread2可以看做是至尊宝和孙悟空。

至尊宝见到紫霞--->threadLocal.set(),紫霞劈开至尊宝的胸膛,造了一颗心并留下泪水(紫霞:泪水)--->紫霞把心(map)塞回至尊宝胸膛

孙悟空见到紫霞--->threadLocal.set(),紫霞劈开孙悟空的胸膛,造了一颗心并留下泪水(紫霞:紫青宝剑)--->紫霞把心(map)塞回孙悟空胸膛

至尊宝又见到紫霞,紫霞拿出 至尊宝的心,取出泪水。

孙悟空又见到紫霞,紫霞拿出 孙悟空的心,取出紫青宝剑。

至尊宝和孙悟空不是同一个人啊,紫霞分别在他们心里放的东西,怎么会串起来呢?

多个Thread与多个ThreadLocal

至尊宝见到紫霞--->threadLocal.set(),紫霞劈开至尊宝的胸膛,造了一颗心并留下泪水(紫霞:泪水)--->紫霞把心(map)塞回至尊宝胸膛

孙悟空见到紫霞--->threadLocal.set(),紫霞劈开孙悟空的胸膛,造了一颗心并留下泪水(紫霞:紫青宝剑)--->紫霞把心(map)塞回孙悟空胸膛

至尊宝又见到紫霞,紫霞拿出至尊宝的心,取出泪水。

孙悟空又见到紫霞,紫霞拿出孙悟空的心,取出紫青宝剑。

悟空去取西经了,杀青了,暂时先忘了他。

至尊宝(Thread)和紫霞(ThreadLocal)快乐地生活着,此时他的心里(ThreadLocalMap)有一颗紫霞的泪水。但有一天他在菜市场遇到了初恋白晶晶(另一个ThreadLocal),白晶晶劈开至尊宝的胸膛,发现已经有心了(ThreadLocalMap),就不造心了,而是在里面又留下一滴泪(白晶晶:泪水)

也就是说,一个Thread只能有一个ThreadLocalMap,第一次遇到的ThreadLocal会帮它创建一个Map塞进去,往后无论遇到多少个ThreadLocal,都是直接用那个Map,而且都是把自己作为key,往Map里存东西。

如果你顺着箭头看,会发现thread-0只能访问threadLocalMap@111,thread-1只能访问threadLoocalMap@222,因为ThreadLocalMap本质是每个Thread内部各存一份,互不干扰。Thread在遇到不同的ThreadLocal,可以把ThreadLocal自身作为key存入map或从map中取出value。

ThreadLocalMap与WeakReference

在Java中有4种引用类型:强、软、弱、虚。

  • 强引用不受GC影响,除非引用全部切断。比如 Student s = new Student(),假设当前只有s指向Student对象,那么当s=null时,Student对象会在下次GC时被回收
  • 软引用对象会在内存不足触发GC时被回收(适用于高速缓存)
  • 弱引用是每次GC时都回收,不论内存是否不足
  • 虚引用(堆外内存,比如zerocopy)

对于Map,每一个键值对被称为Entry,相信大家都知道。

为什么ThreadLocalMap的Entry要继承弱引用呢?

在回答这个问题之前,我们先来了解下弱引用是怎么玩的:

也就是说,当一个对象被WeakReference包装后,它就产生了一个弱引用指向它。此时即使把强引用切断,仍然有弱引用连接着。但是由于弱引用的特性,这个对象会在下次被GC线程被直接回收。

让我们再次回到ThreadLocalMap,虽然Entry继承自WeakReference,但并不是说Entry本身是弱引用,而是Entry的key是弱引用:

那么,ThreadLocalMap为什么要把key包装成弱引用呢?

如果ThreadLocalMap的key不使用Weak Reference,那么堆中的ThreadLocal对象同时存在多处强引用,即使我们把外面的threadLocal设置为null,但ThreadLocalMap中的引用仍然指向堆中的ThreadLocal。最终可能造成内存泄露(无法彻底释放ThreadLocal对象,因为始终有引用指向它)。

而如果key是弱引用,一旦在某一刻,外界所有强引用都被切断(外面的ThreadLocal被置为null),当前只有弱引用指向ThreadLocal对象,那么不久的将来(下一次GC)ThreadLocal对象就会被回收。

ThreadLocalMap与内存泄露

以为我讲重复了吗?上面不是已经用弱引用解决了吗?!

并没有。

原本引入Weak Reference是为了解决多个强引用导致ThreadLocal对象无法回收的问题,但一个解决策略的引入往往伴随着新bug的产生。试想一下,当外部强引用都切断后下一次GC回收了ThreadLocal对象,此时Entry的key会变成什么?

key = null;

当tl1变成null,ThreadLocalMap的Entrys变成下面这样:

  • null : value1(Entry1)
  • tl2 : value2(Entry2)
  • tl3 : value3(Entry3)

从此以后Entry1再也没有人能回收了,因为tl1已经被回收,这个key没了,自然也就无法根据key清除value了。C/C++中有“野指针”的概念,所以我喜欢把这种情况称为“野Entry”。

在引入弱引用前,我们担心的是ThreadLocal一直无法被释放造成内存泄漏,而引入了WeakReference后虽然解决了ThreadLocal的内存泄露,却可能导致Entry的内存泄露,因为当key变成null后,我们无法再根据key移除value了。

实际上,ThreadLocalMap也发现了这个问题,它会在每次get/set时判断key,如果key为null,则把value也归置为null:

但是这种策略是不保险的,因为它的前提是下一次使用时把上一次遗留的key为null的value清除。如果我再也不用,是不是仍然无法移除呢?

所以最保险的方法是,每次使用完毕都及时清除。

ThreadLocal<String> tl = new ThreadLocal<>(); 
tl.set("紫霞"); 
// ...经历过好多事 
tl.remove()

remove()是ThreadLocal的方法,this指的是threadLocal,就是从Map中根据key删除value。

  • tl1 : value1 (根据key把value清空)
  • tl2 : value2
  • tl3 : value3

实际编程时有些公司喜欢在拦截器中取出用户信息放入线程,对此个人建议可以在拦截器的preHandle()中set,在afterCompletion()中remove()。

最后,一张图总结Thread、ThreadLocal和ThreadLocalMap:

---------------------------------------------------------------------------------------------------------------------------------

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

idea一键打包docker镜像并推送远程harbor仓库的方法(包含spotify和fabric8两种方法)--全网唯一正确,秒杀99%水文

我看了很多关于idea一键打包docker镜像并推送harbor仓库的文章&#xff0c;不论国内国外的&#xff0c;基本上99%都是瞎写的&#xff0c; 这些人不清楚打包插件原理&#xff0c;然后就是复制粘贴一大篇&#xff0c;写了一堆垃圾&#xff0c;然后别人拿来也不能用。 然后这篇文…

使用html2canvas转换table为图片时合并单元格rowspan失效,无边框显示问题解决(React实现)

最近使用 html2canvas导出Table表单为图片&#xff0c;但是转换出的图片被合并的单元格没有显示边框 查了原因是因为我为tr设置了背景色&#xff0c;然后td设置了rowspan&#xff0c;设置了rowspan的单元格就会出现边框不显示的问题。 解决方法就是取消tr的背景色&#xff0c;然…

小米手环8pro重新和手机配对解决办法

如果更换了手机&#xff0c;那么小米手环8pro是无法和新手机自动连接的。 但是在新手机上直接连接又连接不上&#xff0c;搜索蓝牙根本找不到手环的蓝牙。 解决办法就是&#xff1a; 把手环恢复出厂&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 是的&…

Ubuntu 20.04 调整交换分区大小

Ubuntu 调整交换分区大小 一、系统情况二、去除旧的交换分区文件三、配置并启用交换分区四、查看swap文件大小 一、系统情况 Ubuntu &#xff1a;Ubuntu 20.04.6 LTS 交换分区位置&#xff1a; cat /proc/swaps二、去除旧的交换分区文件 去掉旧的交换分区有两个步骤&#x…

为什么嵌入式没有35岁危机?

为什么嵌入式没有35岁危机? 在当今数字化时代&#xff0c;IT行业变化迅速&#xff0c;技术的更新迭代速度惊人。然而&#xff0c;有一个技术领域却能够在这个竞争激烈的行业中稳步前行&#xff0c;而且不受35岁危机所困扰&#xff0c;那就是嵌入式技术。 嵌入式技术是指将计算…

Android开发:(AndroidStudio模拟器)如何将模拟器语言设置为中文 模拟器输入法更改为中文输入 键盘输入中文

文章目录 Android开发模拟器设置将模拟器语言设置为中文输入法中文的设置 Android开发模拟器设置 将模拟器语言设置为中文 第一步&#xff1a;打开模拟器后&#xff0c;上滑打开下面的设置图标。 第二步&#xff1a;找到 System (系统) &#xff0c;点击进入。 第三步&am…

外贸自建站怎么做?做外贸要怎样建设网站?

外贸自建站如何建立&#xff1f;海洋建站的具体步骤有哪些&#xff1f; 通过建立自己的外贸网站&#xff0c;您可以更好地展示公司的产品和服务&#xff0c;吸引更多的潜在客户&#xff0c;提高品牌知名度&#xff0c;拓展海外市场。那么&#xff0c;如何建立一个成功的外贸自…

千兆光模块和万兆光模块需要注意哪些事项

随着网络通信技术的发展&#xff0c;千兆光模块和万兆光模块已经成为了网络设备中不可或缺的关键组件。光模块的制造涉及到许多技术和工艺问题&#xff0c;需要严格的控制和管理。本文将从工艺流程、材料选用、测试认证等方面&#xff0c;详细介绍制造千兆光模块和万兆光模块需…

React实战演练项⽬一需求分析及vite_react搭建项目

React实战演练项⽬一需求分析及项目初始化 需求分析 刚学完React,开始找项目进行上手练习&#xff01; 页面组件拆分&#xff1a; 头部&#xff1a;导航tab、搜索框、登录注册 中间&#xff1a;分类导航、轮播图、新人福利、高单价产品导航 课程分类列表、底部内容、登陆提…

android 数独小游戏 经典数独·休闲益智

一款经典数独训练app 标题资源下载 &#xff08;0积分&#xff09;https://download.csdn.net/download/qq_38355313/88544810 首页页面&#xff1a; 1.包含有简单、普通、困难、大师四种难度的数独挑战供选择&#xff1b; 记录页面&#xff1a; 1.记录用户训练过的数独信息&…

使用requests库设置no_proxy选项的方法

问题背景 在使用requests库进行HTTP请求时&#xff0c;如果需要使用爬虫IP服务器&#xff0c;可以通过设置proxies参数来实现。proxies参数是一个字典&#xff0c;其中包含了爬虫IP服务器的地址和端口号。然而&#xff0c;当前的requests库并不支持通过proxies参数来设置no_pr…

map和set的简易封装(纯代码)

RBTree.h #pragma once#include<iostream> #include<vector> using namespace std;enum colar { red,black };template<class T>//有效参数就一个 struct RBTreeNode {RBTreeNode(const T& data):_left(nullptr), _right(nullptr), _parent(nullptr)…

这么好看的马面裙 ,女儿穿上不要太美了

红色小翻领&#xff0c;上身米白色金貂绒面料精细顺滑非常有质感 另外还有全手工定制的盘口裙子用的是仿宋代宋锦的织金面料 制作工艺非常复杂很重工的一件衣服 出门保证会被夸&#xff01;&#xff01;

用Python制作截图小工具

Python编程语言允许我们执行各种任务&#xff0c;所有这些都是在简单模块和短小精悍的代码的帮助下完成的。在Python的帮助下进行屏幕截图就是这样一项任务。 Python为我们提供了许多模块&#xff0c;使我们能够执行不同的任务。有多种方法可以使用Python及其库进行屏幕截图。…

开源电子画册源码系统 可重复利用 适合任何行业 带完整的搭建教程

电子画册&#xff0c;又称电子样本、电子商刊、电子杂志&#xff0c;是一种集合图片处理、文案策划、音乐加工、视频、统计调查、虚拟现实、三维动画等多种技术和表现形式为一体的多媒体画册&#xff0c;电子杂志是纸质印刷画册&#xff08;样本&#xff09;的升级版本&#xf…

创作者等级终于升到4级了

写了两个月的文章&#xff0c;终于等到4级了。发文纪念一下&#xff1a;

划片机:半导体工艺精细化高效化的新里程碑

随着科技的飞速发展&#xff0c;半导体已经成为现代电子设备的基石&#xff0c;而半导体晶圆的划片机作为半导体制造的核心设备之一&#xff0c;其发展程度直接关系到半导体的质量和产量。近年来&#xff0c;博捷芯精密划片机以其在半导体划片机领域的卓越表现&#xff0c;引领…

mybatis、mysql 创建时间(create_time)异常自动更新为当前时间

目录标题 一、问题二、原因三、解决 一、问题 bug: mybatis更新代码没有修改时间&#xff0c;但是时间会自动更新为当前时间。 。。。 被坑了挺久 二、原因 可能是创建表的时候&#xff0c; Navicat Premium 等可视化工具给你整活了。。。 三、解决 取消勾选。 注意&…

R语言基础入门(学习笔记通俗易懂版)

文章目录 R语言预备知识获取工作目录设置工作目录注释变量名的命名赋值变量的显示查看与清除变量函数帮助文档查询函数安装R包文件的读取文件的输出软件的退出与保存 R语言语法向量向量的创建向量的索引&#xff08;向量元素的提取、删除、添加&#xff09;向量长度的获取向量的…

利用 Kubernetes 降本增效?EasyMR 基于 Kubernetes 部署的探索实践

Kubernetes 是用于编排容器化应用程序的云原生系统。最初由 Google 创建&#xff0c;如今由 Cloud Native Computing Foundation&#xff08;CNCF&#xff09;维护更新。 Kubernetes 是市面上最受欢迎的集群管理解决方案之一。它自动化容器化应用程序的部署、扩展和管理&#…