如何正确使用 ThreadLocal,你真的用对了吗? | 京东云技术团队

news2024/11/21 1:36:28

引言:

当多线程访问共享且可变的数据时,涉及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就需要ThreadLocal出场了。

ThreadLocal又称线程本地变量,使用其能够将数据封闭在各自的线程中,每一个ThreadLocal能够存放一个线程级别的变量且它本身能够被多个线程共享使用,并且又能达到线程安全的目的,且绝对线程安全。一般用法如下:

public final static ThreadLocal<String> PARAMS = new ThreadLocal<String>();

PARAMS代表一个能够存放String类型的ThreadLocal对象。此时不论什么一个线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。

实际上可以把企微会话存档的相关配置参数存入到ThreadLocal中,各个方法内需要使用直接从ThreadLocal中获取就可以了.

**原理:**我们先看一下ThreadLocal的结构:

首先是set方法:

这块代码其实很有意思,我们发现在向ThreadLocal中存放值时需要先从当前线程中获取ThreadLocalMap,最后实际是要把当前ThreadLocal对象作为key、要存入的值作为value存放到ThreadLocalMap中,那我们就不得不先看一下ThreadLocalMap的结构。

部分核心代码:

    static class ThreadLocalMap {
        // 键值对实体的存储结构
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            // 当前线程关联的 value,这个 value 并没有用弱引用追踪
            Object value;
            //  k 作 key,作为 key 的 ThreadLocal 会被包装为一个弱引用,v 作 value
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        /**
         * 初始容量,必须为 2 的幂.
         */
        private static final int INITIAL_CAPACITY = 16;
        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
        /**
         * The number of entries in the table.
         */
        private int size = 0;
        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0
}

ThreadLocalMap 是 ThreadLocal 的静态内部类,当一个线程有多个 ThreadLocal 时,需要一个容器来管理多个 ThreadLocal,ThreadLocalMap 的作用就是管理线程中多个 ThreadLocal,从源码中看到 ThreadLocalMap 其实就是一个简单的 Map 结构,底层是数组,有初始化大小,也有扩容阈值大小,数组的元素是 Entry,Entry 的 key 就是 ThreadLocal 的引用,value 是 ThreadLocal内存入 的值。

ThreadLocalMap 解决 hash 冲突的方式采用的是「线性探测法」,如果发生冲突会继续寻找下一个空的位置。

每个Thread内部都持有一个ThreadLoalMap对象

/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */
        ThreadLocal.ThreadLocalMap threadLocals = null;

我们都能够明白ThreadLocal存值的过程了,虽然我们是按照前言中的用法声明了一个全局常量,但是这个常量在每次设置时实际都是向当前线程的ThreadLocalMap内存值,从而确保了数据在不同线程之间的隔离。

接下来就是get:

有了上面的铺垫,这段代码就不难理解了,获取ThreadLocal内的值时,实际上是从当前线程的ThreadLocalMap中以当前ThreadLocal对象作为key取出对应的值,由于值在保存时时线程隔离的,所以现在取值时只会取得当前线程中的值,所以是绝对线程安全的。

remove:

remove将ThreadLocal对象关联的键值对从Entry中移除,正确执行remove方法能够避免使用ThreadLocal出现内存泄漏的潜在风险,int i = key.threadLocalHashCode & (len-1)这行代码很有意思,从一个集合中找到一个元素存放位置的最简单方法就是利用该元素的hashcode对这个集合的长度取余,如果我们能够将集合的长度限制成2的整数次幂就能够将取余运算转换成hashcode与[集合长度-1]的与运算,这样就能够提高查找效率,HashMap中也是这样处理的。

ThreadLocal的原理图:

在提及ThreadLocal使用的注意事项时,所有的文章都会指出内存泄漏这一风险,但是我发现很少有文章能够真正的把这一部分讲清楚,这里我就斗胆尝试一下,由于ThreadLocalMap中的Entry的key持有的是ThreadLocal对象的弱引用,当这个ThreadLocal对象当且仅当被ThreadLocalMap中的Entry引用时发生了GC,会导致当前ThreadLocal对象被回收;那么 ThreadLocalMap 中保存的 key 值就变成了 null,而Entry 又被 ThreadLocalMap 对象引用,ThreadLocalMap 对象又被 Thread 对象所引用,那么当 Thread 一直不销毁的话,value 对象就会一直存在于内存中,也就导致了内存泄漏,直至 Thread 被销毁后,才会被回收。

解决办法:

我们知道出现内存泄漏的原因是失去了对ThreadLocal对象的强引用,避免内存泄漏最简单的方法就是始终保持对ThreadLocal对象的强引用,为每个线程声明一个对ThreadLocal对象的强引用显然是不合适的(太麻烦且缺乏声明的时机),所以,我们可以将ThreadLocal对象声明为一个全局常量,所有的线程均使用这一常量即可,例如:

按照上面的方式声明ThreadLocal对象后,所有的线程共用此对象,在使用此对象存值时会把此对象作为key然后把对应的值作为value存入到当前线程的ThreadLocalMap中,由于此对象始终存在着一个全局的强引用,所以其不会被垃圾回收,调用remove方法后就能够将此对象关联的Entry清除。

结果如下:

可以看出两个线程内对应的Entry的key为同一个对象且即使发生了垃圾回收该对象也不会被回收。

那么是不是说将ThreadLocal对象声明为一个全局常量后使用就没有问题了呢,当然不是,我们需要确保在每次使用完ThreadLocal对象后确保要执行一下该对象的remove方法(重要),清除当前线程保存的信息,这样当此线程再被利用时不会取到错误的信息(使用线程池极易出现);

常见的使用场景:

ThreadLocal 的特性也导致了应用场景比较广泛,主要的应用场景如下:

  • 线程间数据隔离,各线程的 ThreadLocal 互不影响
  • 方便同一个线程使用某一对象,避免不必要的参数传递
  • 全链路追踪中的 traceId 或者流程引擎中上下文的传递一般采用 ThreadLocal
  • Spring 事务管理器采用了 ThreadLocal
  • Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal
  • 一个APP多个数据源,来回切换多个数据源进行查询数据。
  • 日期格式化实例多线程安全问题。

总结:

本文主要从源码的角度解析了 ThreadLocal,并分析了发生内存泄漏的原因及正确用法,最后对它的应用场景进行了简单介绍。

ThreadLocal还有其他变种例如FastThreadLocal和TransmittableThreadLocal,FastThreadLocal主要解决了伪共享的问题比ThreadLocal拥有更好的性能,TransmittableThreadLocal主要解决了线程池中线程复用导致后续提交的任务并不会继承到父线程的线程变量的问题等。

作者:京东零售 郭春元

来源:京东云开发者社区

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

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

相关文章

振弦传感器信号转换器应用山体滑坡安全监测

振弦传感器信号转换器应用山体滑坡安全监测 随着人类文明的进步&#xff0c;自然灾害对人们的生活和财产安全造成的威胁也越来越大。山体滑坡作为自然灾害中的一种&#xff0c;给人们的生活和财产安全带来了极大的威胁。因此&#xff0c;进行山体滑坡的安全监测显得尤为重要。振…

Vue2:基础入门2

Vue2&#xff1a;基础入门2 Date: July 29, 2023 Sum: Computed计算属性、watch侦听器、水果车 计算属性 基础及案例&#xff1a; 概念&#xff1a; 基于现有的数据&#xff0c;计算出来的新属性。 依赖的数据变化&#xff0c;自动重新计算。 计算属性本质上就是一个 func…

C语言中的常量

整型常量 常量是指在程序运行期间其数值不发生变化的数据。整型常量通常简称为整数。 整数可以是十进制数、八进制数和十六进制数。例如&#xff0c;十进制的数值3356可以有下列二种不同的表示形式&#xff1a; 八进制数 06434十六进制数 0xd1c Tip:当我们判断十六进制或八进…

【JavaEE初阶】博客系统后端

文章目录 一. 创建项目 引入依赖二. 设计数据库三. 编写数据库代码四. 创建实体类五. 封装数据库的增删查改六. 具体功能书写1. 博客列表页2. 博客详情页3. 博客登录页4. 检测登录状态5. 实现显示用户信息的功能6. 退出登录状态7. 发布博客 一. 创建项目 引入依赖 创建blog_sy…

Qt编写自定义控件:自定义表头实现左右两端上部分圆角

如上图&#xff0c;左上角和右上角凸出来了。设置表格圆角和表头圆角和QHeaderView::section圆角都不管用。解决此问题需要重写QHeaderView的paintSection()函数&#xff1a; class CustomHeaderView : public QHeaderView { public:explicit CustomHeaderView(Qt::Orientati…

使用toad库进行机器学习评分卡全流程

1 加载数据 导入模块 import pandas as pd from sklearn.metrics import roc_auc_score,roc_curve,auc from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression import numpy as np import math import xgboost as xgb …

Layui实现OA会议系统之会议管理模块总合

目录 一、项目背景 二、项目概述 1. 概述 2. 环境搭建 3. 工具类引用 4. 功能设计 4.1 会议发布 4.2 我的会议 4.3 会议审批 4.4 会议通知 4.5 待开会议 4.6 历史会议 4.7 所有会议 5. 性能优点 5.1 兼容性好 5.2 可维护性和可扩展性 5.3 轻量灵活 5.4 模块化设计…

C++ 第六弹 STL

目录 1.什么是stl 2.六大组件-容器-序列式容器-C98 string 3.六大组件-容器-序列式容器-C98 vector 4.六大组件-容器-序列式容器-C98 list 5.六大组件-容器-序列式容器-C98 deque 6.六大组件-容器-序列式容器-C11 array 7.六大组件-容器-序列式容器-C11 forward_list 8…

Kaggle狗图像分类实战

文章目录 Kaggle狗图像分类实战d2l安装问题python语法学习os.path.joind2l 数据加载streamlit Kaggle狗图像分类实战 d2l安装问题 d2l安装失败&#xff0c;报错如上图 去下面的网站下载到该项目文件目录下再pip install即可 Python d2l项目安装包(第三方库)下载详情页面 - …

若依打印sql

官方issue 自动生成的代码&#xff0c;sql日志怎么没有打印 在ruoyi-admin中的application.yml配置如下。 # 日志配置&#xff0c;默认 logging:level:com.ruoyi: debugorg.springframework: warn#添加配置com.ying: debug输出sql

java 分支控制语句

在程序中&#xff0c;程序运行的流程控制决定程序是如何执行的。 顺序控制 介绍&#xff1a; 程序从上到下的逐行的执行&#xff0c;中间没有任何判断和跳转。 使用&#xff1a;java中定义变量时&#xff0c;采用合法的前向引用。如&#xff1a; public class Test{int num…

成都链安:7月区块链安全事件爆发式增长,导致损失超4.11亿美元

7月&#xff0c;各类安全事件数量及造成的损失较6月爆发式增长。7月发生较典型安全事件超36起&#xff0c;各类安全事件造成的损失总金额约4.11亿美元&#xff08;虚拟货币案件涉案金额除外&#xff09;&#xff0c;较6月上涨约321%。Rug Pull导致损失约2065万美元&#xff0c;…

【linux基础(三)】Linux基本指令(下)

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:Linux从入门到开通⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学更多操作系统知识   &#x1f51d;&#x1f51d; Linux基本指令 1. 前言2. 取头…

HTML 是什么?它的全称是什么?

聚沙成塔每天进步一点点 专栏简介HTML是什么&#xff1f;HTML的全称是什么&#xff1f;写在最后 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅&#xff01;这个专栏是为那些对We…

VMware ESXI虚拟网络和物理网络的对接

探讨VMware ESXI虚拟网络和物理网络的对接 前提&#xff1a; 在上篇搭建了ESXI 6.7&#xff0c;那和VMware Workstation一样想要创建虚拟机前提就必须先创建网络。没有网络连最基本的通信都没有&#xff0c;那肯定不行。所以我们解析要研究一下ESXI的网络。 物理连接模式&am…

Linux Day05

一、库文件生成与使用 1.1库文件 头文件是方法的声明&#xff0c;不是方法的实现 方法的实现是在库&#xff0c;库是预先编译好的方法的集合即.o文件 Linux上的库分为静态库(libxxx.a)和共享库(libxxx.so) 库文件常存放在/lib或者/usr/lib 库对应的头文件一般放在/usr/inc…

【188】Java8利用AVL树实现Map

AVL树又被叫做平衡二叉搜索树、平衡二叉树。AVL是其发明者的首字母缩写。 这篇文章中&#xff0c;AVLTreeMap 类集成了 java.util.Map 接口&#xff0c;并利用 AVL 树结构实现了 Map 接口的所有方法。本文还给出了测试代码。 为什么要发明AVL树&#xff1f; 当我按照从小到大…

【雕爷学编程】MicroPython动手做(33)——物联网之天气预报3

天气&#xff08;自然现象&#xff09; 是指某一个地区距离地表较近的大气层在短时间内的具体状态。而天气现象则是指发生在大气中的各种自然现象&#xff0c;即某瞬时内大气中各种气象要素&#xff08;如气温、气压、湿度、风、云、雾、雨、闪、雪、霜、雷、雹、霾等&#xff…

将自己的网站免费发布到互联网上【无需公网IP】

将自己的网站免费发布到互联网上【无需公网IP】 文章目录 将自己的网站免费发布到互联网上【无需公网IP】将本地搭建的网站发布到互联网步骤 ↓1. 注册并安装cpolar客户端1.1 windows系统1.2 linux系统&#xff08;支持一键自动安装脚本&#xff09;2. 登录cpolar web UI管理界…

Gradio-YOLOv5-YOLOv7 搭建Web GUI

目录 0 相关资料&#xff1a;1 Gradio介绍2 环境搭建3 GradioYOLOv54 GradioYOLOv75 源码解释 0 相关资料&#xff1a; Gradio-YOLOv5-Det&#xff1a;https://gitee.com/CV_Lab/gradio_yolov5_det 【手把手带你实战YOLOv5-入门篇】YOLOv5 Gradio搭建Web GUI: https://www.bi…