ThreadLocal 是 Java 中的一个类,全路径:java.lang.ThreadLocal
,用于在多线程环境下存储线程本地变量。在多线程应用程序中,不同线程之间共享数据可能会引发线程安全问题。
ThreadLocal 通过为每个线程创建独立的变量副本,保证了线程间数据的隔离性,从而有效地解决了这一问题。线程之间的数据访问操作互不影响,提高了多线程应用程序的性能和可靠性。
ThreadLocal 通常与线程池、异步任务和 Web 应用程序等场景结合使用,使得在多线程编程时更加方便和安全。
虽然 ThreadLocal 有这么多好处,但在之前的实际使用中用的并不多,只有在性能测试中的随机数性能问题探索和随机方法性能差异中有所使用。结论分享一下java.util.concurrent.ThreadLocalRandom
这个性能最好。
在近期的测试实践中,又发现了一些有趣的应用场景,分享给大家。
场景一
我有一个工具类,用来去平台获取部分信息。伪代码如下:
package com.funtest.temp
import com.funtester.frame.SourceCode
class ThreadLocalTest extends SourceCode{
/**
* 获取域名
* @return
*/
static String getHost() {
return EMPTY
}
/**
* 获取信息
* @return
*/
static String getMsg() {
return getHost() + "/funtester"
}
/**
* 获取响应
* @param url
* @return
*/
static String getRes(String url) {
return EMPTY
}
}
之所以设计getHost()
这个方法原因是因为不同有不止一个地址所以写了这个方法(可忽略)。本来用来跑任务之后 1 个地址,所以getHost()
只需要写死一个返回值即可。但是新需求来了,每次定时任务需要跑两个环境。
首先想到的思路就是在所有getMsg()
方法里面都添加一个参数,用来标识请求的是哪个环境,这样做看起来比较简单,但是改动地方太多了。而且基于这些方法的脚本已经有不少一直在跑,一旦改动,发布之后还得重新修改脚本。
如果增加一个全局属性,那么在多个线程都在跑任务(假设会有交叉)的时候,这个全局属性就会被多个线程修改,导致线程不安全的问题。
然后我就想到了ThreadLocal
,因为跑任务时候,我都是用线程池去跑,也就是所每个任务都有一个线程。这样我可以针对每个线程设置一个属性。每个线程修改当前线程持有的ThreadLocal
属性又不会影响到其他线程。
真是瞌睡递枕头。
static ThreadLocal<Boolean> Online = new ThreadLocal<Boolean>(){
@Override
protected Boolean initialValue() {
return true
}
}
这样就可以在执行不同的环境时候,调用Online.set()
方法进行修改了。
场景一
这是在Springboot
开发过程中遇到的。原因跟以上有点像,但并不是static
方法。简单描述一下:A 接口参数aBean
,B 接口参数bBean
,A 接口实现中使用到了 B 接口的方法。以此为背景。
某次需求更新,需要在 A 接口增加一项功能,在对某个参数值扩充(这个值不会传递给bBean
),针对新值在调用 B 接口方法的时候特殊处理。
这个时候我又想到了ThreadLocal
,毕竟两件事情没隔几天。索性在 A、B 接口实现类中增加一个ThreadLocal
的成员变量,A 接口在获取到参数时,初始化这个变量。这样 B 接口就可以在处理时,获取变量,进行差别对待了。
虽然这个例子有点牵强,但在实践中确实简单高效的方式。如果改造aBean
和bBean
,势必会导致 B 接口出现多余字段(也可称为非必传字段),在我看来有点不能忍。
所以这虽然不是一个常见的解决方案,但在我的这种场景下,确实一个非常 nice 的方案。成本低,改动小,上线快。
当然也会给后续维护者造成一点点的困惑。这就是另一个故事了。
关于内存泄露
在看资料过程中,很多建议使用者规范使用ThreadLocal
类,容易造成内存泄露。大家可以搜一搜看一看,还是很有必要的。
前面两个例子之所以没有使用规范,原因是因为在执行过程中,多次调用了Online.set()
和Online.get()
方法,会帮助 JVM 回收资源。不用额外调用Online.remove()
主动释放内存。