摘要:ThreadLocal是 java 提供的一个方便对象在本线程内不同方法中传递和获取的类。用它定义的变量,仅在本线程中可见和维护,不受其他线程的影响,与其他线程相互隔离。

本文分享自华为云社区《ThreadLocal:线程专属的变量》,作者: zuozewei 。

ThreadLocal 简介

ThreadLocal是 java 提供的一个方便对象在本线程内不同方法中传递和获取的类。用它定义的变量,仅在本线程中可见和维护,不受其他线程的影响,与其他线程相互隔离。

那 ThreadLocal 到底解决了什么问题,又适用于什么样的场景?

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

核心意思是

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。后文会通过实例详细阐述该观点。另外,该场景下,并非必须使用 ThreadLocal ,其它方式完全可以实现同样的效果,只是 ThreadLocal 使得实现更简洁。

ThreadLocal 使用

ThreadLocal 通过 set 方法可以给变量赋值,通过 get 方法获取变量的值。当然,也可以在定义变量时通过 ThreadLocal.withInitial 方法给变量赋初始值,或者定义一个继承 ThreadLocal 的类,然后重写 initialValue 方法。

下面通过如下代码说明 ThreadLocal 的使用方式:

public class TestThreadLocal
{
private static ThreadLocal<StringBuilder> builder = ThreadLocal.withInitial(StringBuilder::new); public static void main(String[] args)
{
for (int i = 0; i < 5; i++)
{
new Thread(() -> {
String threadName = Thread.currentThread().getName();
for (int j = 0; j < 3; j++)
{
append(j);
System.out.printf("%s append %d, now builder value is %s, ThreadLocal instance hashcode is %d, ThreadLocal instance mapping value hashcode is %d\n", threadName, j, builder.get().toString(), builder.hashCode(), builder.get().hashCode());
} change();
System.out.printf("%s set new stringbuilder, now builder value is %s, ThreadLocal instance hashcode is %d, ThreadLocal instance mapping value hashcode is %d\n", threadName, builder.get().toString(), builder.hashCode(), builder.get().hashCode());
}, "thread-" + i).start();
}
} private static void append(int num) {
builder.get().append(num);
} private static void change() {
StringBuilder newStringBuilder = new StringBuilder("HelloWorld");
builder.set(newStringBuilder);
}
}

在例子中,定义了一个 builder 的 ThreadLocal 对象,然后启动 5 个线程,分别对 builder 对象进行访问和修改操作,这两个操作放在两个不同的函数 append、change 中进行,两个函数访问 builder 对象也是直接获取,而不是放入函数的入参中传递进来。

代码输出如下:

thread-0 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 566157654
thread-0 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 566157654
thread-4 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 654647086
thread-3 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1803363945
thread-2 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1535812498
thread-1 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 2075237830
thread-2 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1535812498
thread-3 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1803363945
thread-4 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 654647086
thread-0 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 566157654
thread-0 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1773033190
thread-4 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 654647086
thread-4 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 700642750
thread-3 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1803363945
thread-3 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1706743158
thread-2 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1535812498
thread-2 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1431127699
thread-1 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 2075237830
thread-1 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 2075237830
thread-1 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1970695360
  • 从输出中 1~6 行可以看出,不同线程访问的是同一个 builder 对象(不同线程输出的 ThreadLocal instance hashcode 值相同),但是每个线程获得的 builder 对象存储的实例 StringBuilder 不同(不同线程输出的 ThreadLocal instance mapping value hashcode 值不相同)。
  • 从输出中1~2、9~10 行可以看出,同一个线程中修改 builder 对象存储的实例的值时,并不会影响到其他线程的 builder 对象存储的实例(thread-4 线程改变存储的 StringBuilder 的值并不会引起 thread-0 线程的 ThreadLocal instance mapping value hashcode 值发生改变)
  • 从输出中 9~13 行可以看出,一个线程对 ThreadLocal 对象存储的值发生改变时,并不会影响其他的线程(thread-0 线程调用 set 方法改变本线程 ThreadLocal 存储的对象值,本线程的 ThreadLocal instance mapping value hashcode 发生改变,但是 thread-4 的 ThreadLocal instance mapping value hashcode 并没有因此改变)。

ThreadLocal 原理

ThreadLocal 能在每个线程间进行隔离,其主要是靠在每个 Thread 对象中维护一个 ThreadLocalMap 来实现的。因为是线程中的对象,所以对其他线程不可见,从而达到隔离的目的。那为什么是一个 Map 结构呢。主要是因为一个线程中可能有多个 ThreadLocal 对象,这就需要一个集合来进行存储区分,而用 Map 可以更快地查找到相关的对象。
ThreadLocalMap 是 ThreadLocal 对象的一个静态内部类,内部维护一个 Entry 数组,实现类似 Map 的 get 和 put 等操作,为简单起见,可以将其看做是一个 Map,其中 key 是 ThreadLocal 实例,value 是 ThreadLocal 实例对象存储的值。

ThreadLocal 适用场景

如上文所述,ThreadLocal 适用于如下场景:

  • 每个线程需要有自己单独的实例,如实现每个线程单例类或每个线程上下文信息(例如事务ID)。
  • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景,提供了另一种扩展 Thread 的方法。如果要保留信息或将信息从一个方法调用传递到另一个方法,则可以使用 ThreadLocal 进行传递。
  • 由于不需要修改任何方法,因此可以提供极大的灵活性。

案例一

这里一个处理 flag 的类,通过 ThreadLocal 使用,可以保证每个请求都拥有唯一的一个追踪标记。

public class TestFlagHolder {

  private final static ThreadLocal<String> TEST_FLAG = new ThreadLocal<>();

  public static void set(String value) {
TEST_FLAG.set(value);
} public static String get() {
return TEST_FLAG.get();
} public static String get4log() {
if (TEST_FLAG.get() == null) {
return "-";
}
return TEST_FLAG.get();
} public static void remove() {
TEST_FLAG.remove();
} }

案例二

在同一线程中 trace 信息的传递:

ThreadLocal<String> traceContext = new ThreadLocal<>();

String traceId = Tracer.startServer();
traceContext.set(traceId) //生成trace信息 传入threadlocal
...
Tracer.startClient(traceContext.get()); //从threadlocal获取trace信息
Tracer.endClient();
...
Tracer.endServer();

案例三

给同一个请求的每一行日志增加一个相同的标记。这样,只要拿到这个标记就可以查询到这个请求链路上所有步骤的耗时了,我们把这个标记叫做 requestId,我们可以在程序的入口处生成一个 requestId,然后把它放在线程的上下文中,这样就可以在需要时随时从线程上下文中获取到 requestId 了。

简单的代码实现就像下面这样:

String requestId = UUID.randomUUID().toString();
ThreadLocal<String> tl = new ThreadLocal<String>(){
@Override
protected String initialValue() {
return requestId;
}
}; //requestId存储在线程上下文中
long start = System.currentTimeMillis();
processA();
Logs.info("rid : " + tl.get() + ", process A cost " + (System.currentTimeMillis() - start)); // 日志中增加requestId
start = System.currentTimeMillis();
processB();
Logs.info("rid : " + tl.get() + ", process B cost " + (System.currentTimeMillis() - start));
start = System.currentTimeMillis();
processC();
Logs.info("rid : " + tl.get() + ", process C cost " + (System.currentTimeMillis() - start));

有了 requestId,你就可以清晰地了解一个调用链路上的耗时分布情况了。

小结

无论是单体系统还是微服务化架构,无论是链路打标还是服务追踪,你都需要在系统中增加 TraceId,这样可以将你的链路串起来,给你呈现一个完整的问题场景。如果 TraceId 可以在客户端上生成,在请求业务接口的时候传递给服务端,那么就可以把客户端的日志体系也整合进来,对于问题的排查帮助更大。

同时,在全链路压测框架中,Trace 信息的传递功能是基于 ThreadLocal 的。但实际业务中可能会使用异步调用,这样就会丢失 Trace 信息,破坏了链路的完整性。所以在实际的项目建议大家不要轻意使用 ThreadLocal。

参考资料:

点击关注,第一时间了解华为云新鲜技术~

知道ThreadLocal吗?一起聊聊到底有啥用的更多相关文章

  1. ThreadLocal 验明正身

    一.前言 之前ThreadLocal使用不多,有个细节也就注意不到了:ThreadLocal在多线程中到底起什么作用?用它保存的变量在每个线程中,是每个线程都保存一份变量的拷贝吗?带着这些问题,我查了 ...

  2. 硬核剖析ThreadLocal源码,面试官看了直呼内行

    工作面试中经常遇到ThreadLocal,但是很多同学并不了解ThreadLocal实现原理,到底为什么会发生内存泄漏也是一知半解?今天一灯带你深入剖析ThreadLocal源码,总结ThreadLo ...

  3. 谁偷了我的热更新?Mono,JIT,iOS

    前言 由于匹夫本人是做游戏开发工作的,所以平时也会加一些玩家的群.而一些困扰玩家的问题,同样也困扰着我们这些手机游戏开发者.这不最近匹夫看自己加的一些群,常常会有人问为啥这个游戏一更新就要重新下载,而 ...

  4. Angularjs进阶笔记(2)-自定义指令中的数据绑定

    有关自定义指令的scope参数,网上很多文章都在讲这3种绑定方式实现的效果是什么,但几乎没有人讲到底怎么使用,本篇希望聊聊到底怎么用这个话题. 一. 自定义指令 自定义指令,是Angularjs用来实 ...

  5. Spring Cloud微服务接口这么多怎么调试

    导读 我们知道在微服务架构下,软件系统会被拆分成很多个独立运行的服务,而这些服务间需要交互通信,就需要定义各种各样的服务接口.具体来说,在基于Spring Cloud的微服务模式中,各个微服务会基于S ...

  6. 通过transmittable-thread-local源码理解线程池线程本地变量传递的原理

    前提 最近一两个月花了很大的功夫做UCloud服务和中间件迁移到阿里云的工作,没什么空闲时间撸文.想起很早之前写过ThreadLocal的源码分析相关文章,里面提到了ThreadLocal存在一个不能 ...

  7. 死磕Java之聊聊ThreadLocal源码(基于JDK1.8)

    记得在一次面试中被问到ThreadLocal,答得马马虎虎,所以打算研究一下ThreadLocal的源码 面试官 : 用过ThreadLocal吗? 楼主答 : 用过,当时使用ThreadLocal的 ...

  8. 聊聊ThreadLocal源码(基于JDK1.8)

    原文:https://cloud.tencent.com/developer/article/1333298 聊聊JDK源码中ThreadLocal的实现 主要方法: ThreadLocal的get方 ...

  9. 聊聊ThreadLocal原理以及使用场景-JAVA 8源码

    相信很多人知道ThreadLocal是针对每个线程的,但是其中的原理相信大家不是很清楚,那咱们就一块看一下源码. 首先,我们先看看它的set方法.非常简单,从当前Thread中获取map.那么这个ge ...

  10. 抛出这8个问题,检验一下你到底会不会ThreadLocal,来摸个底~

    0.问题 和Synchronized的区别 存储在jvm的哪个区域 真的只是当前线程可见吗 会导致内存泄漏么 为什么用Entry数组而不是Entry对象 你学习的开源框架哪些用到了ThreadLoca ...

随机推荐

  1. 虹科干货 | 打破传统!金融界黑科技—虹科Redis企业版数据库

    金融行业数字化转型浪潮来袭,客户需求也正加速向在线金融服务转移.金融机构想要实现现代化改造技术堆栈,为客户提供实时交互.欺诈检测等一系列个性化创新服务,就必须重视遗留系统和传统数据库架构"老 ...

  2. 洛谷P1990

    这是一道dp的题,好像也不算dp.需要递推,感觉能训练思维!!!很棒的一道题. 覆盖墙壁 关于这道题的分析 状态表示:f[i][0]表示前i列全部填满的所有方案,f[i][1]表示前i列全部填满缺一个 ...

  3. 2023寒鹭Tron-CTF迎新赛 CRYPTO Misc 全WP

    CRYPTO 简简单单 1.题目信息 U2FsdGVkX1+2gTXPuTetdM1p+IETUDXAHe2eC33jQfgdJoOmmrJq 2.解题方法 兔子密码,在线工具直接解 简简单单2 1. ...

  4. 自动化混沌工程 ChaosMeta V0.6 版本发布

    混沌工程 ChaosMeta 的全新版本 V0.6.0 现已正式发布!该版本包含了许多新特性和增强功能,在编排界面提供了包括流量注入.度量等各类节点的支持,可视化支撑演练全流程.解决混沌工程原则中&q ...

  5. C函数指针和回调函数

    函数指针 函数指针是指向函数的指针变量. 通常我们说的指针变量是指向一个整型.字符型或数组等变量,而函数指针是指向函数. 函数指针可以像一般函数一样,用于调用函数.传递参数. 函数指针变量的声明: t ...

  6. 开发现代化的.NetCore控制台程序:(2)创建一个C#项目模板

    前言 上一篇文章(开发一个现代化的.NetCore控制台程序,包含依赖注入/配置/日志等要素)介绍了开发现代化的.NetCore控制台程序的细节,但这还不够,我又创建了一个脚手架模板,并命名为 Flu ...

  7. easyxor

    怎么都写不出脚本,看了题解还是蒙蒙的,不过以后遇到这种题也可以根据提示了解他到底怎么出题的,题目明面说了xor 那就主要关注xor以后的结果和xor的对象.方法就好了,其他的就不要去管了 然后对于汇编 ...

  8. Soc的Bring Up流程

    1.Bring Up流程 SOC (System on a Chip) bring-up是一个复杂的过程,涉及到硬件.固件和软件的集成和验证,以下是一个基于BROM,SPL,UBOOT和Linux的启 ...

  9. python3使用pandas备份mysql数据表

    操作系统 :CentOS 7.6_x64 Python版本:3.9.12 MySQL版本:5.7.38 日常开发过程中,会遇到mysql数据表的备份需求,需要针对单独的数据表进行备份并定时清理数据. ...

  10. ELT安装

    前言: ETL是将业务系统的数据经过抽取.清洗转换之后加载到数据仓库的过程, 目的是将企业中的分散.零乱.标准不统一的数据整合到一起,为企业的决策提供分析依据, ETL是BI(商业智能)项目重要的一个 ...