如何正确使用 ThreadLocal,你真的用对了吗?
引言:
当多线程访问共享且可变的数据时,涉及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就需要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主要解决了线程池中线程复用导致后续提交的任务并不会继承到父线程的线程变量的问题等。
作者:京东零售 郭春元
来源:京东云开发者社区
如何正确使用 ThreadLocal,你真的用对了吗?的更多相关文章
- Java_正确理解ThreadLocal
首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的.各 ...
- 正确理解ThreadLocal
想必很多朋友对 ThreadLocal并不陌生,今天我们就来一起探讨下ThreadLocal的使用方法和实现原理.首先,本文先谈一下对ThreadLocal的理 解,然后根据ThreadLocal类的 ...
- 正确理解ThreadLocal:ThreadLocal中的值并不一定是完全隔离的
首先再讨论题主的这个观点之前我们要明确一下ThreadLocal的用途是什么? ThreadLocal并不是用来解决共享对象的多线程访问问题. 看了许多有关ThreadLocal的博客,看完之后会给人 ...
- 详细领悟ThreadLocal变量
关于对ThreadLocal变量的理解,我今天查看一下午的博客,自己也写了demo来测试来看自己的理解到底是不是那么回事.从看到博客引出不解,到仔细查看ThreadLocal源码(JDK1.8),我觉 ...
- 12.ThreadLocal的那点小秘密
大家好,我是王有志.关注王有志,一起聊技术,聊游戏,聊在外漂泊的生活. 好久不见,不知道大家新年过得怎么样?有没有痛痛快快得放松?是不是还能收到很多压岁钱?好了,话不多说,我们开始今天的主题:Thre ...
- ThreadLocal简单理解
在java开源项目的代码中看到一个类里ThreadLocal的属性: private static ThreadLocal<Boolean> clientMode = new Thread ...
- JavaSe:ThreadLocal
JDK中有一个ThreadLocal类,使用很方便,但是却很容易出现问题.究其原因, 就是对ThreadLocal理解不到位.最近项目中,出现了内存泄漏的问题.其中就有同事在使用ThreadLocal ...
- Java中ThreadLocal的深入理解
官方对ThreadLocal的描述: "该类提供了线程局部(thread-local)变量.这些变量不同于它们的普通对应物,因为访问某个变量(通过其get或set方法)的每个线程都有自己的局 ...
- 深入理解ThreadLocal(转)(2015年06月11日)
注明:转自:http://my.oschina.net/clopopo/blog/149368 学习一个东西首先要知道为什么要引入它,就是我们能用它来干什么.所以我们先来看看ThreadLocal对我 ...
- 线程本地变量ThreadLocal
一.本地线程变量使用场景 并发应用的一个关键地方就是共享数据.如果你创建一个类对象,实现Runnable接口,然后多个Thread对象使用同样的Runnable对象,全部的线程都共享同样的属性.这意味 ...
随机推荐
- Netty之数据解码
一.概况 作为Java世界使用最广泛的网络通信框架Netty,其性能和效率是有目共睹的,好多大公司都在使用如苹果.谷歌.Facebook.Twitter.阿里巴巴等,所以不仅仅是因为Netty有高效的 ...
- LeetCode 周赛 343(2023/04/30)结合「下一个排列」的贪心构造问题
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 大家好,我是小彭. 今天是五一假期的第二天,打周赛的人数比前一天的双周赛多了,难道大家都只玩一天吗?这场周赛 ...
- 试用「ChatGPT」几周之后
冷静下来,不吹不黑. 01 最近半年,互联网一款现象级的应用诞生:「ChatGPT」: 其火爆的程度,不输前面的羊了个羊: 最初了解到ChatGPT还是春节的时候,但那时网上的测评还没引起足够的好奇心 ...
- 高精度------C++
高精度运算------C++ (加减乘除) 例:ZOJ2001 http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=1001 The ...
- vue下载附件按钮功能
一.tools文件夹下tools文件中封装下载方法: const iframeId = 'download_file_iframe' function iframeEle (src) { let el ...
- 2022-12-07:删除重复的电子邮箱。删除重复数据后,id=3的数据被删除。请问sql语句如何写? DROP TABLE IF EXISTS `person`; CREATE TABLE `per
2022-12-07:删除重复的电子邮箱.删除重复数据后,id=3的数据被删除.请问sql语句如何写? DROP TABLE IF EXISTS `person`; CREATE TABLE `per ...
- 2022-09-07:给你一个由正整数组成的数组 nums 。 数字序列的 最大公约数 定义为序列中所有整数的共有约数中的最大整数。 例如,序列 [4,6,16] 的最大公约数是 2 。 数组的一个
2022-09-07:给你一个由正整数组成的数组 nums . 数字序列的 最大公约数 定义为序列中所有整数的共有约数中的最大整数. 例如,序列 [4,6,16] 的最大公约数是 2 . 数组的一个 ...
- 2022-08-13:以下go语言代码输出什么?A:[5 6 7 1 2 3 4] B:[1 2 3 4 5 6 7] C:[4 5 6 7 1 2 3]。 package main import
2022-08-13:以下go语言代码输出什么?A:[5 6 7 1 2 3 4] B:[1 2 3 4 5 6 7] C:[4 5 6 7 1 2 3]. package main import ( ...
- SpringBoot 使用 Sa-Token 完成路由拦截鉴权
一.需求分析 在前文,我们详细的讲述了在 Sa-Token 如何使用注解进行权限认证,注解鉴权虽然方便,却并不适合所有鉴权场景. 假设有如下需求:项目中所有接口均需要登录认证校验,只有 "登 ...
- Linux 文件系统inode号和备份恢复
目录 一.inode原理 二.时间类型 三.inode号管理 四.inode实验 五.备份恢复 七.备份实验 一.inode原理 inode只有一个,唯一的,一个文件必须占用一个inode号,但是至少 ...