当前位置: 首页 > news >正文

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的实现原理

直接上图!下图中基本描述出了ThreadThreadLocalMapThreadLocal三者之间的关系。

解释一下:

  • 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无论是在项目开发还是面试中都会经常碰到&#xff0c;它的重要性可见一斑&#xff0c;本篇文章就从ThreadLocal的使用、实现原理、核心方法的源码、内存泄漏问题等展开介绍一下。 一、什么是ThreadLocal ThreadLocal是java.lang下面的一个类&#xff0c;在JDK 1.2版…...

2023复旦大学计算机科学技术(网络空间安全)保研记录

BG&#xff1a;中九rank前5&#xff05;、科研经历少、无竞赛 复旦大学计算机科学与技术--网络空间安全方向&#xff0c;参营4天&#xff08;6.26-6.29&#xff09;&#xff0c;管午饭&#xff0c;住宿自理 6.26--报道听会&#xff0c;6.27--听会&#xff0b;实验室参观 给了…...

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-函数

目录 创建函数 调用函数 参数还是自变量&#xff1f; 参数数量 任意参数&#xff0c;*args 关键字参数 任意关键字参数&#xff0c;**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-内网探针或攻击内网应用&#xff08;触发漏洞地址&#xff09;玩法-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 &#xff0c;通常 Interface 的问题多多少少会遇到&#xff0c;而且可能会遇到一大堆。 在JAVA编程语言中是一个抽象类型&#xff08;Abstract Type&#xff09;&…...

Linux 文件 目录管理

Linux 文件 基本属性 Linux 系统是一种典型的多用户系统&#xff0c;为了保护系统的安全性&#xff0c;不同的用户拥有不同的地位和权限。Linux 系统对不同的用户访问同一文件&#xff08;包括目录文件&#xff09;的权限做了不同的规定。 可以使用命令&#xff1a;ll 或 ls –…...

QT信号槽实现原理

定义Q_OBJECT宏 在宏中声明了几个重要的成员变量及成员函数&#xff0c;包括声明了一个只读的静态成员变量static MetaObject&#xff0c;以及3个public的成员函数 static const QMetaObject staticMetaObject; virtual const QMetaObject *metaObject() const; virtual void …...

7-7 求鸡兔数量

老张家养了很多鸡和兔&#xff0c;圈养在一个笼子里&#xff0c;清早起来老张站在笼子旁边数了数头的个数&#xff0c;蹲下来又数了数脚的个数&#xff0c;你能帮他快速算出来鸡兔各有多少只吗&#xff1f;如实在算不出来&#xff0c; 就提示“error” 输入格式: 输入头的个数…...

CTF 全讲解:[SWPUCTF 2022 新生赛]webdog1__start

文章目录 参考环境题目learning.php信息收集isset()GET 请求查询字符串全局变量 $_GET MD5 绕过MD5韧性脆弱性 md5()弱比较隐式类型转换字符串连接数学运算布尔判断 相等运算符 MD5 绕过科学计数法前缀 0E 与 0e绕过 start.php信息收集头部检索 f14g.php信息收集 探秘 F1l1l1l1…...

聊天机器人

收集窗帘相关的数据 可以用gpt生成&#xff0c;也可以用爬虫 图形化界面 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

数据库题&#xff08;10*5&#xff09; 下面是一个学生与课程的数据库&#xff0c;三个关系表为&#xff1a; 学生表S&#xff08;Sid&#xff0c;SNAME,AGE,SEX&#xff09; 成绩表SC&#xff08;Sid&#xff0c;Cid&#xff0c;GRADE&#xff09; 课程表C&#xff08;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 类型和字符串类型如何相互转换?

在日常开发中&#xff0c;经常需要将数字转换为字符串或者将字符串转换为数字。在 Golang 中&#xff0c;有一些很简便的方法可以实现这个功能&#xff0c;接下来就详细讲解一下如何实现 int 类型和字符串类型之间的互相转换。 使用 strconv 包 strconv 包提供的 Itoa 和 Ato…...

**20.迭代器模式(Iterator)

意图&#xff1a;提供一种方法顺序访问一个聚合对象中的各个元素&#xff0c;而又不需要暴露该对象的内部表示。 上下文&#xff1a;集合对象内部结构常常变化各异。对于这些集合对象&#xff0c;能否在不暴露其内部结构的同时&#xff0c;让外部Client透明地访问其中包含的元素…...

计算机视觉与深度学习 | 视觉里程计理论

===================================================== github:https://github.com/MichaelBeechan CSDN:https://blog.csdn.net/u011344545 ===================================================== 视觉里程计理论基础 1 、立体视觉中的三维测量及几何约束2 、立体视觉匹…...

Go网络请求中配置代理

如何配置代理 不配置代理&#xff0c;本地请求google等会超时 package mainimport ( "fmt" "net/http" "time")func main() { // 创建一个自定义的 Transport 实例 //transport : &http.Transport{ // Proxy: func(req *http.Request) (*url…...

【ArcGIS】基本概念-矢量空间分析

栅格数据与矢量数据 1.1 栅格数据 栅格图是一个规则的阵列&#xff0c;包含着一定数量的像元或者栅格 常用的栅格图格式有&#xff1a;tif&#xff0c;png&#xff0c;jpeg/jpg等 1.2 矢量数据 矢量图是由一组描述点、线、面&#xff0c;以及它们的色彩、位置的数据&#x…...

uniapp 对接腾讯云IM群组成员管理(增删改查)

UniApp 实战&#xff1a;腾讯云IM群组成员管理&#xff08;增删改查&#xff09; 一、前言 在社交类App开发中&#xff0c;群组成员管理是核心功能之一。本文将基于UniApp框架&#xff0c;结合腾讯云IM SDK&#xff0c;详细讲解如何实现群组成员的增删改查全流程。 权限校验…...

eNSP-Cloud(实现本地电脑与eNSP内设备之间通信)

说明&#xff1a; 想象一下&#xff0c;你正在用eNSP搭建一个虚拟的网络世界&#xff0c;里面有虚拟的路由器、交换机、电脑&#xff08;PC&#xff09;等等。这些设备都在你的电脑里面“运行”&#xff0c;它们之间可以互相通信&#xff0c;就像一个封闭的小王国。 但是&#…...

学校招生小程序源码介绍

基于ThinkPHPFastAdminUniApp开发的学校招生小程序源码&#xff0c;专为学校招生场景量身打造&#xff0c;功能实用且操作便捷。 从技术架构来看&#xff0c;ThinkPHP提供稳定可靠的后台服务&#xff0c;FastAdmin加速开发流程&#xff0c;UniApp则保障小程序在多端有良好的兼…...

vue3 定时器-定义全局方法 vue+ts

1.创建ts文件 路径&#xff1a;src/utils/timer.ts 完整代码&#xff1a; import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...

浪潮交换机配置track检测实现高速公路收费网络主备切换NQA

浪潮交换机track配置 项目背景高速网络拓扑网络情况分析通信线路收费网络路由 收费汇聚交换机相应配置收费汇聚track配置 项目背景 在实施省内一条高速公路时遇到的需求&#xff0c;本次涉及的主要是收费汇聚交换机的配置&#xff0c;浪潮网络设备在高速项目很少&#xff0c;通…...

DingDing机器人群消息推送

文章目录 1 新建机器人2 API文档说明3 代码编写 1 新建机器人 点击群设置 下滑到群管理的机器人&#xff0c;点击进入 添加机器人 选择自定义Webhook服务 点击添加 设置安全设置&#xff0c;详见说明文档 成功后&#xff0c;记录Webhook 2 API文档说明 点击设置说明 查看自…...

【C++进阶篇】智能指针

C内存管理终极指南&#xff1a;智能指针从入门到源码剖析 一. 智能指针1.1 auto_ptr1.2 unique_ptr1.3 shared_ptr1.4 make_shared 二. 原理三. shared_ptr循环引用问题三. 线程安全问题四. 内存泄漏4.1 什么是内存泄漏4.2 危害4.3 避免内存泄漏 五. 最后 一. 智能指针 智能指…...

Qemu arm操作系统开发环境

使用qemu虚拟arm硬件比较合适。 步骤如下&#xff1a; 安装qemu apt install qemu-system安装aarch64-none-elf-gcc 需要手动下载&#xff0c;下载地址&#xff1a;https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel/arm-gnu-toolchain-13.2.rel1-x…...

ui框架-文件列表展示

ui框架-文件列表展示 介绍 UI框架的文件列表展示组件&#xff0c;可以展示文件夹&#xff0c;支持列表展示和图标展示模式。组件提供了丰富的功能和可配置选项&#xff0c;适用于文件管理、文件上传等场景。 功能特性 支持列表模式和网格模式的切换展示支持文件和文件夹的层…...

DAY 26 函数专题1

函数定义与参数知识点回顾&#xff1a;1. 函数的定义2. 变量作用域&#xff1a;局部变量和全局变量3. 函数的参数类型&#xff1a;位置参数、默认参数、不定参数4. 传递参数的手段&#xff1a;关键词参数5 题目1&#xff1a;计算圆的面积 任务&#xff1a; 编写一…...