聊聊ThreadLocal(二)

作者简介:大家好,我是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.hqwc.cn/news/185996.html

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

相关文章

U盘不能访问不一定是坏了,可能还有其他原因!U盘无法访问修复详解

当你将USB驱动器连接到计算机时,系统会将其识别为可移动驱动器,并启动其文件管理过程。 然而,用户现在注意到,即使可以检测到他们的USB驱动器,也无法访问它们。 如果幸运的话,拔下插头就能解决问题,但如果不是,请继续阅读以了解更多故障排除选项。 USB闪存驱动器是便…

windows c++开发

一 安装 离线MSDN MSDN:microsoft developer network ,微软向开发人员提供的一套帮助系统。 运行vs 2017 -》运行 vs studio installer ->点击修改-》单个组件-》代码工具-》help viewer-> 安装完后&#xff0c;启动vs 在“帮助”菜单&#xff0c;“设置帮助首选项…

git 指定时间代码统计

指定时间代码统计 用法 13 - 17 号 代码情况 近一周 git log --since2023-11-13 00:00:00 --until2023-11-17 23:00:00 --prettytformat: --numstat | awk { add $1; subs $2; loc $1 - $2 } END { printf "added lines: %s, removed lines: %s,total lines: %s\n&…

SaaS与PaaS平台的区别

目录 一、前言 二、SaaS化与PaaS化平台的区别 三、PaaS化的低代码平台更胜一筹 PaaS优势&#xff1a; 支持PaaS服务的低代码平台 1.私有化部署&#xff0c;为数据安全保驾护航 2.业内领先技术&#xff0c;为开发强势赋能 3.超强集成能力&#xff0c;系统对接无忧 4.源代码交付&…

关于圆通物流在AppLink上的操作

在使用物流系统时&#xff0c;我们会出现订单变化而导致物流轨迹发生改动&#xff0c;如果反馈不及时会造成额外的工作量以及会出现人为的操作失误&#xff0c;我们尝试借助AppLink来实现圆通物流在发生变化时的信息同步 登录开放平台 复制右侧登录地址登录圆通速递管理后台&…

内衣洗衣机和手洗哪个干净?性价比高的迷你洗衣机推荐

洗衣机已经成为了每户每家的生活中必不可少的家用电器&#xff0c;在最近的几年以来&#xff0c;家用洗衣机的技术得到了广泛的发展&#xff0c;这不但给了消费者一个干净的生活环境&#xff0c;也让每一个家庭都享受到了科技的便捷。人们对传统的大尺寸的洗衣机已经很熟悉了。…

jvm 内存结构 ^_^

1. 程序计数器 2. 虚拟机栈 3. 本地方法栈 4. 堆 5. 方法区 程序计数器 定义&#xff1a; Program Counter Register 程序计数器&#xff08;寄存器&#xff09; 作用&#xff0c;是记住下一条jvm指令的执行地址 特点&#xff1a; 是线程私有的 不会存在内存溢出 虚拟机栈…

IaaS、PaaS、SaaS 的区别

文章1&#xff1a;软件系统的分类 文章2&#xff1a;有哪些通俗易懂的例子可以解释 IaaS、PaaS、SaaS 的区别&#xff1f;——原文 写的很好的两篇文章值得一看 你一定听说过云计算中的三个“高大上”的你一定听说过云计算中的三个“高大上”的概念&#xff1a;IaaS、PaaS和…

Windows安装nvm【node.js版本管理工具】

目录 下载安装包 安装 配置 配置node的国内镜像源 配置npm的国内镜像源 常用命令 查看可安装的node版本 安装指定的版本 查看已有的node版本列表 切换版本 下载安装包 https://github.com/coreybutler/nvm-windows/releases/tag/1.1.11 安装 安装过程就不贴了&#xff0…

UE基础篇八:平衡蓝图与C++的使用

一、蓝图转换C++ 案例结构: 1.1 蓝图和C++对比 1.2 将蓝图变量转C++ 现在C++中定义同样的类型

7.docker运行redis容器

1.准备redis的配置文件 从上一篇运行MySQL容器我们知道&#xff0c;需要给容器挂载数据卷&#xff0c;来持久化数据和配置&#xff0c;相应的redis也不例外。这里我们以redis6.0.8为例来实际说明下。 1.1 查找redis的配置文件redis.conf 下面这个网址有各种版本的配置文件供…

制造业工厂的MES系统数据采集功能概述

一、MES系统与数据采集 MES系统是专门针对制造业工厂的信息化管理系统&#xff0c;旨在提高生产效率、降低成本、优化资源配置。数据采集作为MES系统的重要功能之一&#xff0c;能够实时获取生产现场的数据信息&#xff0c;为生产管理提供可靠的决策依据。 二、MES数据采集功能…