跟对领导很重要:愿意教你的,并且放手让你做的领导要珍惜。
前言
ThreadLocal
:线程 + 本地 -> 线程本地变量(所以说我觉得它取名叫ThreadLocalVariable
获取还更能让人易懂些),它的出镜率可不低。虽然写业务代码一般用不着,但它是开源工具的常客:用于在线程生命周期内传递数据。
有的人说,每看一遍ThreadLocal
都会有新的感受,这其实是比较诡异的现象,因为我认为“真理”是不应该经常变的(或者说是不可能变化的)。我自己百度了一波,关于ThreadLocal
的文章满天飞,有讲使用的亦有讲原理的,鱼龙混杂。其中有一派文章主旨讲述:使用ThreadLocal
解决多线程程序的并发问题,使用该工具写出简洁、优美的多线程程序…
这类水文不在少数,大有占据主流的意思,它对初学者的误导性非常大,从而造成了每看一遍都会有新感受的错觉。本文为社区贡献一份微薄之力,在这里教你完全正确的使用ThreadLocal
的姿势,避免你以后再犯迷糊。
正文
本文的内容并不讲述ThreadLocal/InheritableThreadLocal
的源码、原理,一方面确实不难,另一方面关于它的源码、原理讲解的相关文章确实不在少数。
ThreadLocal是什么?
我们从字面上的意思来理解ThreadLocal
,Thread:线程;Local:本地的,局部的。也就是说,ThreadLocal
是线程本地的变量,只要是本线程内都可以使用,线程结束了,那么相应的线程本地变量也就跟随着线程消失了。
ThreadLocal
和InheritableThreadLocal
均是JDK1.2新增的API,在JDK5后支持到了泛型。它表示线程局部变量:为当前线程绑定一个变量,这样在线程的声明周期内的任何地方均可取出。
说明:
InheritableThreadLocal
继承自ThreadLocal
,在其基础上扩展了能力:不仅可在本线程内获取绑定的变量,在子线程内亦可获取到。(请注意:必须是子线程,若你是线程池就可能不行喽,因为线程池里的线程是实现初始化好的,并不一定是你的子线程~)
它仅有如下三个public
方法:
public void set(T value) { ... }
public T get() { ... }
public void remove() { ... }
分别代表:
- 设置值:把value和当前线程绑定
- 获取值:获取和当前线程绑定的变量值
- 删除值:移除绑定关系
说明:虽然每个绑定关系都是使用的
WeakReference
,但是还是建议你显示的做好remove()
移除动作,否则容易造成内存泄漏。当然关于ThreadLocal
内存泄漏并不是本文的内容,有兴趣可以自行去了解。
另外对于解释ThreadLocal
是什么,建议可参考下它的Javadoc:
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code 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).
大致意思是:
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量
(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。
ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程
(例如,用户 ID 或事务 ID)相关联。
更准确的说,一般我们使用ThreadLocal
是作为private static final
字段来使用的~
ThreadLocal怎么用?
知道了ThreadLocal
是什么后,怎么用其实就非常简单了。看如下这个简单示例:
本例模拟使用Person对象和当前线程绑定:
@Setter
@ToString
private static class Person {
private Integer age = 18;
}
书写测试代码:
private static final ThreadLocal<Person> THREAD_LOCAL = new ThreadLocal<>();
@Test
public void fun1() {
// 方法入口处,设置一个变量和当前线程绑定
setData(new Person());
// 调用其它方法,其它方法内部也能获取到刚放进去的变量
getAndPrintData();
System.out.println("======== Finish =========");
}
private void setData(Person person){
System.out.println("set数据,线程名:" + Thread.currentThread().getName());
THREAD_LOCAL.set(person);
}
private void getAndPrintData() {
// 拿到当前线程绑定的一个变量,然后做逻辑(本处只打印)
Person person = THREAD_LOCAL.get();
System.out.println("get数据,线程名:" + Thread.currentThread().getName() + ",数据为:" + person);
}
运行程序打印输出:
set数据,线程名:main
get数据,线程名:main,数据为:Test2.Person(age=18)
======== Finish =========
这便是ThreadLocal
的典型应用场景:方法调用间传参,并不一定必须得从方法入参处传入进来,还可以通过ThreadLocal
来传递,进而在该线程生命周期内任何地方均可获取到,非常的方便有木有。
小细节:set和get数据时的线程是同一个线程:均未main线程
局限性
上例是ThreadLocal
的典型应用场景,大部分情况下均能正常work。但是,在当下互联网环境下,经常会用到了异步方式来提高程序运行效率,比如如上方法调用getAndPrintData()
因比较耗时所以我希望异步去进行,改造如下:
@Test
public void fun1() throws InterruptedException {
// 方法入口处,设置一个变量和当前线程绑定
setData(new Person());
// getAndPrintData();
// 异步获取数据
Thread subThread = new Thread(() -> getAndPrintData());
subThread.start();
subThread.join();
// 非异步方式获:在主线程里获取
getAndPrintData();
System.out.println("======== Finish =========");
}
运行程序,打印输出:
set数据,线程名:main
get数据,线程名:Thread-0,数据为:null
get数据,线程名:main,数据为:Test2.Person(age=18)
======== Finish =========
线程名为Thread-0
的子线程里并没有获取到数据,只因为它并不是当前线程,貌似合情合理,这便是ThreadLocal
的局限性。
那既然这是一个常见需求,除了把变量作为方法入参传进去,有没有什么更为便捷的方案呢?有的,JDK扩展了ThreadLocal
提供了一个子类:InheritableThreadLocal
,它能够向子线程传递数据。
InheritableThreadLocal向子线程传递数据
它继承自ThreadLocal
,所以它能力更强:通过它set进去的数据,不仅本线程内任意地方可以获取,子线程(包括子线程的子线程…)内的任意地方也都可以获取到值。
重说三:必须是子线程,必须是子线程,必须是子线程(当然子线程的子线程…也算作这个范畴)
因此对于上例,只需做个微小的变化:
// 使用InheritableThreadLocal作为实现
private static final ThreadLocal<Person> THREAD_LOCAL = new InheritableThreadLocal<>();
再次运行测试程序,打印:
set数据,线程名:main
get数据,线程名:Thread-0,数据为:Test2.Person(age=18)
get数据,线程名:main,数据为:Test2.Person(age=18)
======== Finish =========
完美。
强调说明:其实这还不完美。还有非父子线程、垮线程池之间的数据产地它解决不了,对于这种场景较为复杂,源生JDK并没有“特效类”,一般需要借助阿里巴巴的开源库:TTL(
transmittable-thread-local
)来搞定,这个后面文章还会继续补充。
开源框架使用示例
优秀的开源框架中有非常多的对ThreadLocal
的使用示例,这里以Spring的为例:
RequestContextHolder
:
public abstract class RequestContextHolder {
...
private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal<>("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal<>("Request context");
...
@Nullable
public static RequestAttributes getRequestAttributes() {
RequestAttributes attributes = requestAttributesHolder.get();
if (attributes == null) {
attributes = inheritableRequestAttributesHolder.get();
}
return attributes;
}
...
}
TransactionSynchronizationManager
:
public abstract class TransactionSynchronizationManager {
...
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal<>("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal<>("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal<>("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal<>("Actual transaction active");
...
}
如果你们公司做过全链路追踪、全链路压测,那么ThreadLocal
将是其中使用最为频繁的基础组件之一。
ThreadLocal不能解决共享变量的线程安全问题
标题即是结论,请务必烂熟于胸,使用时请勿滥用。网上太多的文章说:ThreadLocal
使得每个线程均持有这个变量的副本,所以对多线程是安全的。言外之意便是:
- 只要这个变量是共享变量,把它用
ThreadLocal
包起来便可 - 别的线程修改其线程绑定的变量,并不影响其它线程里的变量值
以上结果,如果你的ThreadLocal
绑定的是Immutable
不可变变量,如字符串等,那结论尚能成立,但若绑定的是引用类型的变量,结论可就大错特错喽,如下示例:
private static final ThreadLocal<Person> THREAD_LOCAL = new InheritableThreadLocal<>();
@Test
public void fun2() throws InterruptedException {
setData(new Person());
Thread subThread1 = new Thread(() -> {
Person data = getAndPrintData();
if (data != null)
data.setAge(100);
getAndPrintData(); // 再打印一次
});
subThread1.start();
subThread1.join();
Thread subThread2 = new Thread(() -> getAndPrintData());
subThread2.start();
subThread2.join();
// 主线程获取线程绑定内容
getAndPrintData();
System.out.println("======== Finish =========");
}
private void setData(Person person) {
System.out.println("set数据,线程名:" + Thread.currentThread().getName());
THREAD_LOCAL.set(person);
}
private Person getAndPrintData() {
// 拿到当前线程绑定的一个变量,然后做逻辑(本处只打印)
Person person = THREAD_LOCAL.get();
System.out.println("get数据,线程名:" + Thread.currentThread().getName() + ",数据为:" + person);
return person;
}
对本实例模拟的场景做如下文字解释:
- 主线程设置一个共享变量Person(age=18),希望子线程得以共享
- 创建两个子线程
subThread1/subThread2
用于模拟多个线程,共享访问Person这个共享变量 - 线程
subThread1
在其执行过程中,把共享变量Person的age值改为了100 - 线程
subThread2
以及主线程此时也去获取共享变量Person,情况如何呢?
运行测试程序,打印如下:
set数据,线程名:main
get数据,线程名:Thread-0,数据为:Test2.Person(age=18)
get数据,线程名:Thread-0,数据为:Test2.Person(age=100)
get数据,线程名:Thread-1,数据为:Test2.Person(age=100)
get数据,线程名:main,数据为:Test2.Person(age=100)
======== Finish =========
看到这个结果,你或许会傻眼。不是拷贝了一个副本吗,为何最终值也变了呢?可以明确的告诉你,这不是ThreadLocal
有错,而是你没有理解它。
结论:线程subThread1
把共享变量Person的值改过之后,其它线程再去获取得到的均是改变后的值,因此此处使用ThreadLocal
并没有达到决绝共享变量线程安全问题的效果。
这是最为典型的一种错误认知,希望通过此例能帮你纠正你以前对ThreadLocal
的理解和看法(有错则改之嘛~)。
为何会出现此现象,是因为这里面所谓的变量副本都是“引用传递”来着,可以用如下程序证明:
@Test
public void fun3() throws InterruptedException {
setData(new Person());
new Thread(() -> System.identityHashCode(THREAD_LOCAL.get())).start();
new Thread(() -> System.identityHashCode(THREAD_LOCAL.get())).start();
TimeUnit.SECONDS.sleep(2);
System.out.println(System.identityHashCode(THREAD_LOCAL.get()));
System.out.println("======== Finish =========");
}
运行程序,控制台输出:
set数据,线程名:main
434455603
434455603
434455603
======== Finish =========
可以看到,不管是主线程还是子线程,绑定的变量的HashCode一模一样。这样就更能解释了:为何一处修改,其它均被修改了吧,因为指向是同一位置。
因此:ThreadLocal
包装根本就不能解决共享变量的多线程安全问题。
ThreadLocal使用的正确姿势
说了这么多,那使用它的正确姿势是什么呢?正确姿势用文字无法表达,请以如下使用示例为参照。
众所周知,SimpleDateFomat
是线程不安全的,所以若我们这样定义一个全局模版:
public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
在多线程访问的情况下,那必然会有多线程安全问题。
而通过如上表述,这么做也依旧是不靠谱的,依旧解决不了多线程安全问题。
public static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new InheritableThreadLocal<>();
static {
DATE_FORMAT_THREAD_LOCAL.set(new SimpleDateFormat("yyyy-MM-dd"));
}
其实关于它的使用,阿里巴巴已经在它的规范手册里给出了使用示范:
public static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new InheritableThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
这么处理后再使用DateFormat
这个实例,就是绝对安全的。理由是每次调用set方法进行和线程绑定的时候,都是new一个新的SimpleDateFormat
实例,而并非全局共享一个,不存在了数据共享那必然就线程安全喽。
当然你可能会说,这和自己在线程里面每次你自己new一个出来用有什么区别呢?答案是:效果上没任何区别,但是这样方便。比如:可以保持线程里面只有唯一一个SimpleDateFormat
对象,你要手动new的话,一不小心就new多个了,消耗内存不说还不好管理。
可能你还会说,那只new一个实例,然后哪个方法要用就通过参数传给它就行。答案还是一样的:不嫌麻烦的话,这样做也是能达到效果的。
然而,对于这种全局通用的变量,使用ThreadLocal
管理和维护一份即可,大大的降低了维护成本和他人的使用成本。so,只要你使用它的姿势正确了,它能让你事半功倍,特别是如果你是写中间件的小伙伴的话,跟它打交道会更为频繁。
总结
本文总体上算是一篇纠错文章,希望更多人能够看到,多多转发,为社区献上微薄之力。
ThreadLocal
并不是为了解决线程安全问题,而是提供了一种将变量绑定到当前线程的机制,类似于隔离的效果。ThreadLocal
跟线程安全基本不搭边:线程安全or不安全取决于绑上去的实例是怎样的:
- 每个线程独享一份new出来的实例 -> 线程安全
- 多个线程共享一份“引用类型”实例 -> 线程不安全
ThreadLocal
最大的用处就是用来把实例变量共享成全局变量,在程序的任何方法中都可以访问到该实例变量而已。网上很多人说ThreadLocal
是解决了线程安全问题,大都是望文生义,二者完全非同类问题,读者需要有自己的思考呀。
声明
原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭
。你也可【左边扫码/或加wx:fsx641385712】邀请你加入我的 Java高工、架构师 系列群大家庭学习和交流。
- [享学Jackson] 一、初识Jackson – 世界上最好的JSON库
- [享学Jackson] 二、jackson-core之流式API与JsonFactory、JsonGenerator、JsonParser
- [享学Jackson] 三、jackson-databind之ObjectMapper与数据绑定、树模型
- [享学Jackson] 四、控制Jackson行为的特征们之JsonFactory.Feature、JsonGenerator.Feature、JsonParser.Feature
- [享学Jackson] 五、控制Jackson行为的特征们之JsonWriteFeature、JsonReadFeature
- [享学Jackson] 六、控制Jackson行为的特征们之MapperFeature、SerializationFeature、DeserializationFeature
- [享学Jackson] 七、Jackson使用bit位运算来开启/禁用Feature的原理解析
- [享学Jackson] 八、jackson-databind数据绑定基础配置之BaseSettings、MapperConfig、MapperConfigBase
- [享学Jackson] 九、jackson-databind数据绑定序列化/反序列化配置之SerializationConfig、DeserializationConfig
- [享学Jackson] 十、jackson-databind序列化之ObjectMapper序列化原理、序列化器匹配原理
- [享学Jackson] 十一、jackson-databind之JsonSerializer序列化器全解析
- [享学Jackson] 十二、jackson-databind反序列化之ObjectMapper反序列化原理、JsonDeserializer反序列化器全解析
- [享学Jackson] 十三、jackson-annotation注解模块全解析及Jackson注解大全
- [享学Jackson] 十四、深入理解Jackson的Module模块化设计及原理分析
- [享学Jackson] 十五、第三方模块Module的深度实践:JavaTimeModule、JSR310Module、ParameterNamesModule、Jdk8Module
- [享学Jackson] 十六、Jackson在Spring MVC中的使用之Date、JSR310时间类型的处理
- [享学Jackson] 十七、spring-web整合Jackson源码解析之Jackson2ObjectMapperBuilder
- [享学Jackson] 十八、Spring容器深度整合Jackson的桥梁之SpringHandlerInstantiator
- [享学Jackson] 十九、Spring下使用ObjectMapper的正确姿势 — Jackson2ObjectMapperFactoryBean
- [享学Jackson] 二十、Spring MVC下的Jackson — MappingJackson2HttpMessageConverter
- [享学Jackson] 二十一、Spring Boot下的Jackson — JacksonAutoConfiguration自动配置
- [享学Jackson] 二十二、Jackson与Fastjson的恩怨情仇(完结篇)
转载:https://blog.csdn.net/f641385712/article/details/104573489