Java并发编程第8讲——ThreadLocal详解
ThreadLocal无论是在项目开发还是面试中都会经常碰到,它的重要性可见一斑,本篇文章就从ThreadLocal的使用、实现原理、核心方法的源码、内存泄漏问题等展开介绍一下。
一、什么是ThreadLocal
ThreadLocal是java.lang下面的一个类,在JDK 1.2版本加入,作者是Josh Bloch(集合大神)和Doug Lea(并发大神)。
它提供了一种线程局部变量的方式,线程局部变量是指每个线程都拥有自己独立的变量副本,互不干扰,通过ThreadLocal,可以方便地在多线程环境下共享数据,同时不需要考虑线程安全性,这也是解决并发问题的途径之一。
例如:在web开发中,可以使用ThreadLocal来保存用户的登录信息,以便每个线程都能够独立地获取和修改自己的登录信息,避免了线程之间的干扰。
二、ThreadLocal的使用
ThreadLocal有四个方法,分别为:
protected T initialValue():返回此线程局部变量的初始值。
pubulic T get(): 返回当前线程局部变量的当前线程副本的值。如果这是线程第一次调用该方法,则创建并初始化此副本。
public void set(T value):将此线程局部变量的当前线程的副本设置为指定的值。
public void remove():移除此线程局部变量的当前线程的值。
下面使用ThreadLocal来模拟用户登录信息的场景:
ThreadLocal工具类:
public class CurrentUserHolder {public static ThreadLocal<User> threadLocal=new ThreadLocal<>();
public static void setUser(User user){threadLocal.set(user);}
public static User getUser(){if (Objects.nonNull(threadLocal.get())) {return threadLocal.get();}throw new RuntimeException("当前用户信息为空!");}
public static void clearUser(){threadLocal.remove();}
}
User实体类:
@Data
public class User {private String name;private Integer age;
}
测试:
public class Test {public static void main(String[] args) {//用户登录User user = new User();user.setName("小黑子");user.setAge(18);//将用户信息保存在ThreadLocal中CurrentUserHolder.setUser(user);//在其它方法中,可以通过ThreadLocal获取用户信息User localUser = CurrentUserHolder.getUser();System.out.println(localUser);//输出:User(name=小黑子, age=18)//用户操作完成后,可以remove掉CurrentUserHolder.clearUser();}
}
ps:由于ThreadLocal是基于线程的,所以在不同的线程中,通过ThreadLocal获取的用户信息是独立的,这在多线程环境下非常有用,可以避免线程之间的数据混乱和冲突。
三、ThreadLocal的实现原理
直接上图!下图中基本描述出了Thread、ThreadLocalMap和ThreadLocal三者之间的关系。

解释一下:
ThreadLocal中用于保存线程的独有变量的数据结构是一个内部类:ThreadLocalMap,也是k-v结构,key就是当前ThreadLocal对象,value就是我们要保存的值。
Thread类中维护了两个ThreadLocalMap成员变量,threadLocals和inheritableThreadLocals,它们的默认值是null,类型为ThreadLocal.ThreadLocalMap,也就是ThreadLocal类的一个静态内部类ThreadLocalMap,感兴趣的可以去看一下源码。
四、核心源码
4.1 ThreadLocalMap内部类
在静态内部类ThreadLocalMap中,维护了一个数据结构类型为Entry的数组,源码如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}
从源码中我们可以看到,Entry继承了一个ThreadLocal类型的弱引用并将其作为key,value为Object类型(也就是我们需要保存的值)。
我们再来看一下它的成员变量:
//数组的默认初始化容量
private static final int INITIAL_CAPACITY = 16;
//Entry数组,大小必须为2的幂
private Entry[] table;
//数组内部元素个数
private int size = 0;
//数组扩容阈值,默认为0,创建ThreadLocalMap对象后会被重新设置
private int threshold;
是不是有点熟悉,这几个变量和HashMap中的变量很类似,功能也类似。
最后看一下它的构造方法:
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);
}
注释翻译过来大概就是,该构造方法是懒加载的,只有我们创建一个Entry对象并需要放入到Entry数组的时候才会去初始化数组。
4.2 set()方法
接下来我们就介绍一下ThreadLocal常用的一些方法吧,首先看一下set()方法:
public void set(T value) {//获取当前线程Thread t = Thread.currentThread();//获取当前线程的ThreadLocalMap对象ThreadLocalMap map = getMap(t);if (map != null)// 如果map存在,则将当前ThreadLocal对象作为key,value作为value放入map中map.set(this, value);else// 如果map不存在,则创建一个新的ThreadLocalMap对象,并新建一个Entry放入该ThreadLocalMap, 调用set方法的ThreadLocal和传入的value作为该Entry的key和valuecreateMap(t, value);
}
解释:
获取当前线程,拿到当前Thread的ThreadLocalMap对象。
如果map存在,则将当前ThreadLocal对象作为key,value作为value放入map中。
如果map不存在,则创建一个新的ThreadLocalMap对象,并新建一个Entry放入该ThreadLocalMap, 调用set方法的ThreadLocal和传入的value作为该Entry的key和value。
4.3 get()方法
源码如下:
public T get() {//获取当前线程Thread t = Thread.currentThread();//获取当前线程的ThreadLocalMap对象ThreadLocalMap map = getMap(t);if (map != null) {//map存在,通过this(当前ThreadLocal)获取EntryThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")//Entry不为空,返回该Entry的value值T result = (T)e.value;return result;}}//map不存在,调用setInitialValue()方法设置初始值return setInitialValue();
}
解释:
通过当前线程获取ThreadLocalMap:
如果map存在,则通过当前ThreadLocal获取对应的Entry,若Entry不为空,返回该Entry的value值。
如果map不存在,则调用setInitialValue()方法设置初始值。
setInitialValue():
根据initalValue()方法获取value值,默认值为null,可以重写该方法。
通过当前线程获取ThreadLocalMap对象。
map存在,设置当前值为上述value,不存在则创建新的ThreadLocalMap,并将值设置为value。
4.4 remove()方法
源码如下:
public void remove() {//根据当前线程获取ThreadLocalMap对象ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)//存在,执行remove方法m.remove(this);
}
解释:
根据当前线程获取ThreadLocalMap对象,存在则执行remove()方法。remove(this)方法中,将ThreadLocal作为key来删除对应的Entry。
五、内存泄漏问题
5.1 分析
读到这,相信你对ThreadLocal的基本原理有了更深一步的理解,我们把上图补全,从堆栈视角看一下它们之间的引用关系。

我们可以看到,ThreadLocal对象,有两个引用,一个是栈上的ThreadLocal引用,一个是ThreadLocalMap中Key对它的引用。如果栈上的ThreadLocal引用不再使用了,那么ThreadLocal对象因为还有一条引用链在,所以会导致它无法回收,久而久之就会OOM。
这就是我们所说的ThreadLocal的内存泄漏问题,为了解决这个问题,ThreadLocalMap使用了弱引用,就是上述我们说过的Entry数组:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}
可以看出,ThreadLocal的引用k通过构造方法传递给了Entry类的父类WeakReference的构造方法,那么可以理解为ThreadLocalMap中的键是ThreadLocal的弱引用。
穿插一下Java中的四大引用:
强引用:Java中默认的引用类型,只要引用还存在,即便OOM也不会被回收。
软引用:内存不足时,将会被干掉。
弱引用:无论内存充足与否,只要执行GC,就会被干掉。
虚引用:最弱的一种引用,存在意义就是为了将关联虚引用的对象在被GC掉之后收到一个通知。
如果用了弱引用,那么ThreadLocal对象就可以在下次GC的时候被回收掉了。

这样做可以很大程度上避免了因为ThreadLocal的使用而导致的OOM问题,但也无法彻底避免。
我们可以看到,虽然key是弱引用,但是value是强引用,而且它的生命周期是和Thread一样的,也就是说,只要Thread还在,那么这个对象就无法被回收。
那么,什么情况下,Thread会一直在呢,那就是线程池,这就导致value一直无法被回收。
5.2 如何解决
ThreadLocalMap底层使用数组来保存元素,使用“线性探测法”来解决hash冲突,在每次调用ThreadLocal的get、set、remove方法时,内部会实际调用ThreadLocalMap的get、set、remove等操作,而ThreaLocalMap的每次set、get、remove时,都会对key为null的Entry进行清除(expungeStateEntry()方法,将Entry的value清空,等下次GC就会被回收)。
所以,当我们一个ThreadLocal用完后,就手动remove一下,就可以在下次GC时,把Entry清理掉。
5.3 总结
上述我们分了两种情况来看ThreadLocal内存泄漏问题:
key使用强引用:引用ThreadLocal的对象被回收了,但是ThreadLocalMap持有ThreadLocal的强引用,如果没有手动remove,ThreadLocal不会被回收,导致Entry内存泄漏。
key使用弱引用:引用ThreadLocal被回收,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动remove,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动remove,就会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal被清理后key为null,对应的value在下一次ThreadLocalMap调用set、get、remove的时候可能会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期和Thread一样长,如果没有手动remove就会导致内存泄漏,而不是因为弱引用。
End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。
相关文章:
Java并发编程第8讲——ThreadLocal详解
ThreadLocal无论是在项目开发还是面试中都会经常碰到,它的重要性可见一斑,本篇文章就从ThreadLocal的使用、实现原理、核心方法的源码、内存泄漏问题等展开介绍一下。 一、什么是ThreadLocal ThreadLocal是java.lang下面的一个类,在JDK 1.2版…...
2023复旦大学计算机科学技术(网络空间安全)保研记录
BG:中九rank前5%、科研经历少、无竞赛 复旦大学计算机科学与技术--网络空间安全方向,参营4天(6.26-6.29),管午饭,住宿自理 6.26--报道听会,6.27--听会+实验室参观 给了…...
linux系统通过docker安装python的jieba,如何找到jieba路径替换分词文件
1、安装python镜像 python镜像名为 jetz_python3.7.131、进入容器 首次安装镜像后,容器启动,进入容器中,其中py37是容器名称,后面会一直用到 docker run -it --name py37 jetz_python3.7.13 /bin/bash如果进入过容器退出了,而容器已存在,上面的的 命令会报错,直接根…...
Python Functions-函数
目录 创建函数 调用函数 参数还是自变量? 参数数量 任意参数,*args 关键字参数 任意关键字参数,**kwargs 默认参数值 将列表作为参数传递 The pass Statement 递归 函数是一个只有在被调用时才运行的代码块。 可以将称为参数的数…...
【人工智能】机器学习的入门与提升
目录 1.入门 1.1.从何处开始 1.2.数据集 1.3.数据类型 2.平均中位数模式 2.1.均值、中值和众数 2.2.均值 2.2.1.实例 2.2.2.运行结果 2.3.中值 2.3.1.实例 2.3.2.运行结果 2.3.3.实例 2.3.4.运行结果 2.4.众数 2.4.1.实例 2.4.2.运行结果 2.5.章节总结 3.标准…...
WEB漏洞原理之---【XMLXXE利用检测绕过】
文章目录 1、概述1.1、XML概念1.2、XML与HTML的主要差异1.3、XML代码示例 2、靶场演示2.1、Pikachu靶场--XML数据传输测试玩法-1-读取文件玩法-2-内网探针或攻击内网应用(触发漏洞地址)玩法-3-RCE引入外部实体DTD无回显-读取文件开启phpstudy--apache日志…...
element-table排序icon没有点亮
<el-table :data"tableData" ref"tableRef"border :sort"defaultSort":default-sort"defaultSort"><el-table-column sortable :sort-orders"sortOrder" prop"date" label"日期"> </el-…...
传统的经典问题 Java 的 Interface 是干什么的
传统的经典问题 Java 的 Interface 是干什么 解答 上面的这个问题应该还是比较好回答的吧。 只要你做过 Java ,通常 Interface 的问题多多少少会遇到,而且可能会遇到一大堆。 在JAVA编程语言中是一个抽象类型(Abstract Type)&…...
Linux 文件 目录管理
Linux 文件 基本属性 Linux 系统是一种典型的多用户系统,为了保护系统的安全性,不同的用户拥有不同的地位和权限。Linux 系统对不同的用户访问同一文件(包括目录文件)的权限做了不同的规定。 可以使用命令:ll 或 ls –…...
QT信号槽实现原理
定义Q_OBJECT宏 在宏中声明了几个重要的成员变量及成员函数,包括声明了一个只读的静态成员变量static MetaObject,以及3个public的成员函数 static const QMetaObject staticMetaObject; virtual const QMetaObject *metaObject() const; virtual void …...
7-7 求鸡兔数量
老张家养了很多鸡和兔,圈养在一个笼子里,清早起来老张站在笼子旁边数了数头的个数,蹲下来又数了数脚的个数,你能帮他快速算出来鸡兔各有多少只吗?如实在算不出来, 就提示“error” 输入格式: 输入头的个数…...
CTF 全讲解:[SWPUCTF 2022 新生赛]webdog1__start
文章目录 参考环境题目learning.php信息收集isset()GET 请求查询字符串全局变量 $_GET MD5 绕过MD5韧性脆弱性 md5()弱比较隐式类型转换字符串连接数学运算布尔判断 相等运算符 MD5 绕过科学计数法前缀 0E 与 0e绕过 start.php信息收集头部检索 f14g.php信息收集 探秘 F1l1l1l1…...
聊天机器人
收集窗帘相关的数据 可以用gpt生成,也可以用爬虫 图形化界面 gradio 向量数据库 faiss python代码 import gradio as gr import random import timefrom typing import Listfrom langchain.embeddings.openai import OpenAIEmbeddings from langchain.vectorstor…...
肖sir__mysql之综合题练习__013
数据库题(10*5) 下面是一个学生与课程的数据库,三个关系表为: 学生表S(Sid,SNAME,AGE,SEX) 成绩表SC(Sid,Cid,GRADE) 课程表C(Cid&…...
阿里云服务器部署安装hadoop与elasticsearch踩坑笔记
2023-09-12 14:00——2023.09.13 20:06 目录 00、软件版本 01、阿里云服务器部署hadoop 1.1、修改四个配置文件 1.1.1、core-site.xml 1.1.2、hdfs-site.xml 1.1.3、mapred-site.xml 1.1.4、yarn-site.xml 1.2、修改系统/etc/hosts文件与系统变量 1.2.1、修改主机名解…...
Golang 中 int 类型和字符串类型如何相互转换?
在日常开发中,经常需要将数字转换为字符串或者将字符串转换为数字。在 Golang 中,有一些很简便的方法可以实现这个功能,接下来就详细讲解一下如何实现 int 类型和字符串类型之间的互相转换。 使用 strconv 包 strconv 包提供的 Itoa 和 Ato…...
**20.迭代器模式(Iterator)
意图:提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。 上下文:集合对象内部结构常常变化各异。对于这些集合对象,能否在不暴露其内部结构的同时,让外部Client透明地访问其中包含的元素…...
计算机视觉与深度学习 | 视觉里程计理论
===================================================== github:https://github.com/MichaelBeechan CSDN:https://blog.csdn.net/u011344545 ===================================================== 视觉里程计理论基础 1 、立体视觉中的三维测量及几何约束2 、立体视觉匹…...
Go网络请求中配置代理
如何配置代理 不配置代理,本地请求google等会超时 package mainimport ( "fmt" "net/http" "time")func main() { // 创建一个自定义的 Transport 实例 //transport : &http.Transport{ // Proxy: func(req *http.Request) (*url…...
【ArcGIS】基本概念-矢量空间分析
栅格数据与矢量数据 1.1 栅格数据 栅格图是一个规则的阵列,包含着一定数量的像元或者栅格 常用的栅格图格式有:tif,png,jpeg/jpg等 1.2 矢量数据 矢量图是由一组描述点、线、面,以及它们的色彩、位置的数据&#x…...
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造,完美适配AGV和无人叉车。同时,集成以太网与语音合成技术,为各类高级系统(如MES、调度系统、库位管理、立库等)提供高效便捷的语音交互体验。 L…...
2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...
线程同步:确保多线程程序的安全与高效!
全文目录: 开篇语前序前言第一部分:线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分:synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分ÿ…...
深入浅出:JavaScript 中的 `window.crypto.getRandomValues()` 方法
深入浅出:JavaScript 中的 window.crypto.getRandomValues() 方法 在现代 Web 开发中,随机数的生成看似简单,却隐藏着许多玄机。无论是生成密码、加密密钥,还是创建安全令牌,随机数的质量直接关系到系统的安全性。Jav…...
JVM垃圾回收机制全解析
Java虚拟机(JVM)中的垃圾收集器(Garbage Collector,简称GC)是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象,从而释放内存空间,避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...
五年级数学知识边界总结思考-下册
目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解:由来、作用与意义**一、知识点核心内容****二、知识点的由来:从生活实践到数学抽象****三、知识的作用:解决实际问题的工具****四、学习的意义:培养核心素养…...
跨链模式:多链互操作架构与性能扩展方案
跨链模式:多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈:模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展(H2Cross架构): 适配层…...
Swagger和OpenApi的前世今生
Swagger与OpenAPI的关系演进是API标准化进程中的重要篇章,二者共同塑造了现代RESTful API的开发范式。 本期就扒一扒其技术演进的关键节点与核心逻辑: 🔄 一、起源与初创期:Swagger的诞生(2010-2014) 核心…...
Python 包管理器 uv 介绍
Python 包管理器 uv 全面介绍 uv 是由 Astral(热门工具 Ruff 的开发者)推出的下一代高性能 Python 包管理器和构建工具,用 Rust 编写。它旨在解决传统工具(如 pip、virtualenv、pip-tools)的性能瓶颈,同时…...
FFmpeg:Windows系统小白安装及其使用
一、安装 1.访问官网 Download FFmpeg 2.点击版本目录 3.选择版本点击安装 注意这里选择的是【release buids】,注意左上角标题 例如我安装在目录 F:\FFmpeg 4.解压 5.添加环境变量 把你解压后的bin目录(即exe所在文件夹)加入系统变量…...
