news 2026/3/27 20:07:11

浅谈ThreadLocal----每个线程一个小书包

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
浅谈ThreadLocal----每个线程一个小书包

hreadLocal是什么?

thread是线程,local是本地的意思

字面意思是线程本地。

其实更通俗的理解是给每个线程设置一个缓存。这个缓存用来存储当前线程在未来的业务逻辑中需要执行到的变量。

我们先来看怎么用:

首先创建全局变量ThreadLocal,

各自启动一个线程任务:

线程任务将变量设置到缓存中。

线程任务需要用到缓存中的变量时,直接从缓存中取即可。

复制代码

1 import java.util.concurrent.TimeUnit;

2

3 /**

4 * @discription

5 */

6 public class ThreadLocalLearn {

7 static ThreadLocal<String> threadLocal = new ThreadLocal<>();

8

9 public static void main(String[] args) {

10 Runnable r = new Runnable() {

11 @Override

12 public void run() {

13 threadLocal.set(Thread.currentThread().getName());

14 sayMyName();

15 threadLocal.remove();

16 }

17

18 public void sayMyName() {

19 for (int i = 0; i < 3; i++) {

20 String name = threadLocal.get();

21 System.out.println(Thread.currentThread().getName() + " say: im a thread, name:" + name);

22 try {

23 TimeUnit.SECONDS.sleep(3);

24 } catch (Exception e) {

25 //...

26 }

27 }

28 }

29 };

30 Thread t1 = new Thread(r);

31 t1.start();

32 Thread t2 = new Thread(r);

33 t2.start();

34 }

35 }

复制代码

它的使用非常简单,

(1)先set()存储值;

(2)使用时get()取出值;

(3)用完了使用remove()清理掉;

输出如下:

复制代码

Connected to the target VM, address: '127.0.0.1:56863', transport: 'socket'

Thread-0 say: im a thread, name:Thread-0

Thread-1 say: im a thread, name:Thread-1

Thread-0 say: im a thread, name:Thread-0

Thread-1 say: im a thread, name:Thread-1

Thread-1 say: im a thread, name:Thread-1

Thread-0 say: im a thread, name:Thread-0

Disconnected from the target VM, address: '127.0.0.1:56863', transport: 'socket'

复制代码

很多人第一次见到ThreadLocal,第一直觉它的实现是用Map<Thread,Object> 。(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )但是深入研究之后,你会发现threadLocal的实现要比这样一个map 精妙的多,也好用的多。

我们通过查看java源码,可以依次探索ThreadLocal是如何实现缓存的:

类整体的关系大概是这样的:

tlleitu

查看源码,我们可以发现如下特性:

1、ThreadLocal本身并不是缓存,它只是起到一个缓存的key 的作用。我们每次创建一个ThreadLocal 并不是真正的创建了一个缓存,其实只是创建了一个缓存的标识。

源码如下:this 就是ThreadLocal实例

复制代码

1 public void set(T value) {

2 Thread t = Thread.currentThread();

3 ThreadLocalMap map = getMap(t);

4 if (map != null) {

5 map.set(this, value);

6 } else {

7 createMap(t, value);

8 }

9 }

复制代码

2、真正的缓存保存在Thread中,缓存被定义为:

ThreadLocal.ThreadLocalMap threadLocals;

从名字可以发现,这个缓存的类型是在ThreadLocal 中定义的一个静态内部类。这个类就是用来真正存放缓存的地方。这就像是thread小书包一样,每个线程有一个自己的独立的存储空间。

设计疑问:它(ThreadLocalMap)为什么没有定义在Thread类中,毕竟它是Thread的缓存。

源码如下:Thread.java

1 /* ThreadLocal values pertaining to this thread. This map is maintained

2 * by the ThreadLocal class. */

3 ThreadLocal.ThreadLocalMap threadLocals = null;

3、查看ThreadLocalMap的源码,我们发现它并没有实现Map接口,就像其他map一样,ThreadLocalMap实现了常用的Map中的set,get,getEntry,setThreshold,,remove 等方法。

并且它内部使用了线性探测法来解决哈希冲突。

设计疑问:它(ThreadLocalMap)为什么没有实现Map接口?

源码如下:ThreadLocal.Java

复制代码

1 static class ThreadLocalMap {

2

3 //...

4

5 private static final int INITIAL_CAPACITY = 16;

6

7

8 private Entry[] table;

9

10

11 private int size = 0;

12

13

14 private int threshold; // Default to 0

15

16

17 private void setThreshold(int len) {

18 threshold = len * 2 / 3;

19 }

20

21

22 private Entry getEntry(ThreadLocal<?> key) {

23 ...

24 }

25

26

27

28 private void set(ThreadLocal<?> key, Object value) {

29 ...

30 }

31

32

33 private void remove(ThreadLocal<?> key) {

34 ...

35 }

36

37

38 private void rehash() {

39 ...

40 }

41

42 private void resize() {

43 ...

44 }

45 ....

46 }

复制代码

4、继续看源码,我们发现ThreadLocalMap类像其他Map实现一样,在内部定义了Entry。并且这个Entry居然继承了弱引用,弱引用被定义在Entry的key上,而且key的类型是ThreadLocal。

至于什么是弱引用,我以前的文章中介绍过,请看(浅谈Java中的引用 https://www.cnblogs.com/jilodream/p/6181762.html),一定要对弱引用了解,否则ThreadLocal的核心实现以及它会存在的问题,就无法更深理解了。

这里又会有疑问,为什么要使用弱引用,使用强引用不好吗?弱引用万一被回收导致空引用等问题怎么办?

源码如下:ThreadLocal.Java

复制代码

1 static class Entry extends WeakReference<ThreadLocal<?>> {

2 /** The value associated with this ThreadLocal. */

3 Object value;

4

5 Entry(ThreadLocal<?> k, Object v) {

6 super(k);

7 value = v;

8 }

9 }

复制代码

我们依次回答这几个问题:

(1)设计疑问:它(ThreadLocalMap)为什么没有定义在Thread类中,毕竟它是Thread的缓存

这恰恰是Thread符合开闭原则的优秀设计。如果是将ThreadLocalMap添加到Thread中,那么Thread类就太重了,以后只要和线程相关的业务都要将代码添加到Thread中,那Thread就无限膨胀了,变成超级类了,试想什么业务和线程能脱离关系呢?

况且他们只是类依赖关系而不是组合关系(对类关系不了解的同学可以看我的这篇文章:统一建模语言UML---类图 https://www.cnblogs.com/jilodream/p/16693511.html)。

Map怎么实现,缓存怎么维护,这些都是Thread不需要考虑的,我们就是需要用到你的特性。

(2)设计疑问:它(ThreadLocalMap)为什么没有实现Map接口?

实现接口是为了统一化提供接口,让外界可以只依赖接口,而不是接口的实现。但是ThreadLocalMap并不是给外界使用的,并不需要暴露出来。他就是为了给ThreadLocal业务使用的。只要完成最核心的Map能力,用空间换时间,将理论时间复杂度推向O(1)即可。因此完全没有必要实现Map接口。实现了Map接口反而要将内部方法暴露为public,这也不符合最少知道原则。一句话就是没必要,还添乱。

(3)为什么要使用弱引用,使用强引用不好吗?弱引用万一被回收导致空引用等问题怎么办?

我们需要先了解弱引用的特性:当一个变量只有弱引用关联时,那么在下次GC回收时,不论我们内存是否足够,都将回收掉该内存。

第一眼感觉这很危险,毕竟我们非常担心就是一个变量用着用着突然不能用了,出现空引用了,漫天的空引用这太不可控了。

其实这完全多虑了,注意看:我们是如何使用缓存的,是通过threadlocal.get(),也就是说我们想要使用缓存就一定要使用threadlocal的实例,也就是强引用,

有了强引用,使用时就一定不会被回收。因此完全不用担心使用缓存中,弱引用key突然变为null的情况了。

那什么时候弱引用key会被回收呢?

这就是当外界的强引用被手动设置为null时,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )或者是作为局部变量跳出了方法栈,超出生命周期被回收掉了。

试想一下,真要是发生这两种情况,那么其实这个缓存也就根本无法再用到了同时,key被尽快回收,反而对内存更有利。

那么弱引用这么好用,为什么value不设置为弱引用呢?

其实细想一下就会发现value一定不能设置为弱引用,为什么呢?

key设置为弱引用,是因为想要使用这个缓存,key就一定要有强引用关联。而value则不一定有外界强引用关联,它在外界的强引用可能早就消失了。比如下面这个例子:

复制代码

1 import java.util.concurrent.TimeUnit;

2

3 /**

4 * @discription

5 */

6 public class ThreadLocalLearn {

7 static ThreadLocal<UserInfo> userContext = new ThreadLocal<>();

8

9 public static void main(String[] args) {

10 Runnable r = new Runnable() {

11 @Override

12 public void run() {

13 setUserInfo();

14 handle();

15 userContext.remove();

16 }

17

18 public void handle() {

19 UserInfo user = userContext.get();

20 //注意倘若map中的value被定义为弱引用,则此处的user可能为null

21 System.out.println(" i am:" + user.toString());

22 //do sth

23 try {

24 TimeUnit.SECONDS.sleep(3);

25 } catch (Exception e) {

26 //...

27 }

28 }

29 };

30 Thread t1 = new Thread(r);

31 t1.start();

32 Thread t2 = new Thread(r);

33 t2.start();

34 }

35

36 private static void setUserInfo() {

37 UserInfo user = new UserInfo();// 假装是从db中获取的

38 userContext.set(user);

39 //跳出该方法后,userInfo的在外部的直接强引用就被回收了

40 }

41 }

42

43 class UserInfo {

44 private String name;

45 private int age;

46

47 //....

48 }

复制代码

我们在A方法中设置了缓存 currentUserId,跳出A方法,currentUserId在外界的引用被断开,倘若此时value也被定义为弱引用,value就随时可能被回收。而我们又可以通过

(key)Threadlocal --> threadLocals(ThreadLocalMap) --> entry --> value

这样的调用关系来拿到缓存value。这样缓存的使用就不可控了。

那么value一定不能设置为弱引用或及时回收么?

并不是,

其实我们只要在key回收时,顺手对value也做一个回收,但是这是GC完成的,再key消失时,联动对所有线程中关联的Map都进行一遍清理。(实现过于复杂)

亦或者清理key(threadlocal)的强引用时,将value的强引用也一并被清理。

可行,也是ThreadLocal推荐的方式,需要手动调用ThreadLocal.remove 方法。

在调用remove方法后,ThreadLocalMap会对所有垃圾数据进行清理,还会压缩哈希表。

为了解决ThreadLocalMap的value 延迟清理的情况,ThreadLocalMap在set get remove等方法中,都会对ThreadLocalMap存在的这种<null,Object> 垃圾数据进行一定程度的清理(注意这里要分各种情况,具体只能详细分析源码了,一篇博文很难说清)。

(4)这样又会有一个新的问题,如果key 被回收了,但是value没有被回收,因此value就常驻内存了,那么value不就会导致内存泄露吗?

很不幸,这样的确是会导致内存的泄露。(这里简单提一下,java中的内存泄露是指,可以通过强引用关联到他,gc无法回收掉它。与此同时,业务按照正常逻辑又无法使用到它。也就是又用不到,又回收不掉,就称之为内存泄露)

但是这种内存泄露出现的概率非常低。

它需要同时满足以下三个条件才可以:

1、需要线程的生命周期永远不会结束。如果线程生命周期结束了,那么ThreadLocalMap就会被回收,里边出现的无其他关联的key value 也都会被回收。

这种一般是守护线程或者线程池(线程复用出现)

2、ThreadLocal在设置为null时,没有手动调动remove方法

3、线程中的ThreadLocalMap在后续使用中,没有再调用任何get set remove方法,也就是线程没再使用ThreadLocal

概率低,是不是代表不太需要关注,当然不是。

因为内存泄露不仅仅是减少了可用内存,还增加了GC负担,系统性能就会收到影响,这就说的远了。

其实ThreadLocal最大的问题,并不是泄露的问题,而是被滥用的问题,不规范使用的问题。很多人把ThreadLocal当成是线程的私有仓库,所有变量参数都往里边塞,

导致写代码和维护时,非常不方便,出现问题也给维护人员造成很大的困扰。

接下来我们简单说下ThreadLocal的使用(后边我会再写一篇,如何使用ThreadLocal,毕竟我们学习技术目的是能够驾驭它,而不仅仅是知其所以然):

我们一般是将上下文信息,或者当前需要频繁使用的,与实际业务直接关系不大的系统数据方便携带。放置到thread的小书包中。

(1)上下文信息

如我们在controller层,将用户的上下文信息传入,如traceId(方便链路追踪),如用户token,后续可能调用其他鉴权接口等

(2)解耦数据库连接等连接池信息,

比如Springboot运行事务时,我们每次getconnection(),就只使用ThreadLocal中贮存好的这个连接,整个方法使用的是同一个数据库连接。

以上场景不使用ThreadLocal可以吗?

也可以,他并不是一定要使用。但是你这样就要把很多的参数传来传去,暴露很多的问题。

甚至在很多第三方实现的框架中,他不支持你传这些参数,他就是要用通过ThreadLocal来回传值。

(3)为线程安全提供了方案,减少了锁竞争:

如果说锁是从资源竞争的角度,解决了数据安全的问题。

ThreadLocal则是在每个线程中,只保存(只隔离)出与自己当前业务相关的数据。

注意他只是保证了数据的独立性,并不是独立创建了一份副本,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )所以如果使用全局数据放置到value中时,一样可能会有数据安全问题。(当然这也是不推荐的用法)

比如有一份UserCache的全局缓存,多线程使用时,

我可以在全局中对UserCache进行加锁处理,也可以每个线程独立引用自己的UserInfo,线程之间互不干扰。结构就像这个样子:

全局加锁:

tljingzheng

线程各自引用:

tlyinyong

不知讲到这里大家还有没有最初的直觉了,为啥不设计一个全局的 Map<Thread,Object>。这样不是更简单,也更好定位问题:

细想一下,就会发现这样并不好:

方案1,全局只有一个Map,value是当前线程的所有缓存数据。那么Object就是一个非常复杂的数据,每次对Object进行读取都要解析的

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/24 17:38:59

定性与定量考核的结合

在现代企业管理中&#xff0c;如何科学、公正地评估员工绩效&#xff0c;始终是一个核心议题。要实现全面而准确的评估&#xff0c;关键在于将定量考核的客观性与定性考核的深刻性有效结合。 单纯的定量考核&#xff08;“计件”&#xff09;提供了“做什么”的客观数据&#x…

作者头像 李华
网站建设 2026/3/27 9:11:16

如何衡量团队产出效率

在现代组织中&#xff0c;团队的产出效率直接决定企业的竞争力与执行力。**要科学衡量团队产出效率&#xff0c;核心在于建立多维度的指标体系&#xff0c;将成果、过程与协作因素综合评估&#xff0c;以实现对绩效的量化与优化。**单纯用“工作量”或“加班时间”衡量团队贡献…

作者头像 李华
网站建设 2026/3/27 4:48:57

使用格子玻尔兹曼方法(LBM)模拟热扩散的Matlab代码

使用格子玻尔兹曼方法&#xff08;LBM&#xff09;模拟热扩散&#xff0c;Matlab代码格子玻尔兹曼方法&#xff08;LBM&#xff09;搞热扩散模拟其实挺有意思的&#xff0c;今天咱们用Matlab整一个简单的二维版本。先上核心思路&#xff1a;把温度场当作被动标量&#xff0c;用…

作者头像 李华
网站建设 2026/3/27 4:12:53

ORACLE学习笔记总结(数据库参数文件)

Oracle数据库参数文件详解与操作指令 一、参数文件类型概述 Oracle数据库使用两种参数文件来存储实例配置&#xff1a; 1. PFILE&#xff08;Parameter File&#xff09; 文件类型&#xff1a;文本文件&#xff0c;可直接编辑 默认名称&#xff1a;init<SID>.ora&…

作者头像 李华
网站建设 2026/3/27 17:02:36

浅谈:算法中的斐波那契数(六)

方法五&#xff1a;矩阵求幂斐波那契数列矩阵方程&#xff1a;算法&#xff1a;若 N 小于等于 1&#xff0c;则返回 N。使用递归函数matrixPower 计算给定矩阵 A 的幂。幂为 N-1&#xff0c;其中 N 是第 N 个 斐波那契数。matrixPower 函数将对 N/2 个斐波那契数进行操作。在 m…

作者头像 李华