ThreadLocal 实践与源码解析 ——Java
ThreadLocal 实践与源码解析
refer author: 写代码的SharkChili
写在文章开头
在多线程编程中,共享资源的管理和同步一直是开发人员面临的挑战之一。ThreadLocal
是 Java
提供的一种简单而强大的机制,用于实现线程局部变量,即每个线程都有自己的独立副本,互不干扰。这种机制不仅简化了并发编程中的数据管理,还提高了代码的可读性和可维护性。
Hi,我是 sharkChili ,是个不断在硬核技术上作死的技术人,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。
同时也非常欢迎你star我的开源项目mini-redis:https://github.com/shark-ctrl/mini-redis
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。
详解ThreadLocal
什么是ThreadLocal?它有什么用?
为了保证特定变量对当前线程可见,我们就可以使用ThreadLocal
关键字,ThreadLocal
可以为每个线程创建这个变量的副本并存到每个线程的存储空间中(关于这个存储空间后文会展开讲述)
,从而确保共享变量对每个线程隔离:
### ThreadLocal基础使用示例
如上文所说ThreadLocal最典型的用法就是维护各个线程各自需要独享变量,基于ThreadLocal
为每个将每个线程的id
存到线程内部,彼此之间互不影响。
ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
Thread t1 = new Thread(() -> {
log.info("t1往THREAD_LOCAL存入变量:[{}]", Thread.currentThread().getName());
THREAD_LOCAL.set(Thread.currentThread().getName());
log.info("t1获取THREAD_LOCAL的值为:[{}]", THREAD_LOCAL.get());
}, "t1");
Thread t2 = new Thread(() -> {
log.info("t2往THREAD_LOCAL存入变量:[{}]", Thread.currentThread().getName());
THREAD_LOCAL.set(Thread.currentThread().getName());
log.info("t2获取THREAD_LOCAL的值为:[{}]", THREAD_LOCAL.get());
THREAD_LOCAL.remove();
log.info("t2删除THREAD_LOCAL的后值为:[{}]", THREAD_LOCAL.get());
}, "t2");
t1.start();
t2.start();
ThreadUtil.sleep(1,TimeUnit.DAYS);
从输出结果可以看出,两个线程都用THREAD_LOCAL 在自己的内存空间中存储了变量的副本,彼此互相隔离的使用
21:59:51.351 [t2] INFO MultiApplication - t2往THREAD_LOCAL存入变量:[t2]
:59:51.351 [t1] INFO MultiApplication - t1往THREAD_LOCAL存入变量:[t1]
:59:51.358 [t1] INFO MultiApplication - t1获取THREAD_LOCAL的值为:[t1]
:59:51.359 [t2] INFO MultiApplication - t2获取THREAD_LOCAL的值为:[t2]
:59:51.359 [t2] INFO MultiApplication - t2删除THREAD_LOCAL的后值为:[null]
从两种应用场景来介绍一下ThreadLocal
日期格式化工具类
我们创建100个线程使用同一个dateFormat
完成日期格式化:
private static Logger logger = LoggerFactory.getLogger(MyThreadLocalDemo3.class);
static SimpleDateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS");
public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(100);
for (int i = 0; i < 100; i++) {
int finalI = i;
//线程池中的线程
threadPool.submit(()->{
new MyThreadLocalDemo3().caclData(finalI);
});
}
threadPool.shutdown();
}
/**
* 计算second后的日期
* @param second
* @return
*/
public String caclData(int second){
Date date=new Date(1000*second);
String dateStr = dateFormat.format(date);
logger.info("{}得到的时间字符串为:{}",Thread.currentThread().getId(),dateStr);
return dateStr;
}
从输出结果可以看出,间隔几毫秒的线程出现相同结果
基于该问题我们使用ThreadLocal为线程分配SimpleDateFormat副本:
static ThreadLocal<SimpleDateFormat> threadLocal=ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS"));
public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(100);
for (int i = 0; i < 100; i++) {
int finalI = i;
//线程池中的线程
threadPool.submit(()->{
new MyThreadLocalDemo3().caclData(finalI);
});
}
threadPool.shutdown();
}
/**
* 计算second后的日期
* @param second
* @return
*/
public String caclData(int second){
Date date=new Date(1000*second);
SimpleDateFormat simpleDateFormat = threadLocal.get();
String dateStr = simpleDateFormat.format(date);
logger.info("{}得到的时间字符串为:{}",Thread.currentThread().getId(),dateStr);
return dateStr;
}
服务间调用的线程变量共享
我们日常web开发都会涉及到各种service
的调用,例如某个controller需要调用完service1
之后再调用service2
。因为我们的controller
和service
都是单例的,所以如果我们希望多线程调用这些controller
和service保证共享变量的隔离,也可以用到ThreadLocal
。
为了实现这个示例,我们编写了线程获取共享变量的工具类:
public class MyUserContextHolder {
private static ThreadLocal<User> holder = new ThreadLocal<>();
public static ThreadLocal<User> getHolder() {
return holder;
}
}
service
调用链示例如下,笔者创建service1
之后,所有线程复用这个service
完成了调用,并且在服务间调用直接通过ThreadLocal
完成了线程副本共享:
public class MyThreadLocalGetUserId {
private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);
private static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
int finalI = i;
MyService1 service1 = new MyService1();
threadPool.submit(() -> {
service1.doWork1("username" + (finalI+1));
});
}
}
}
class MyService1 {
private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);
public void doWork1(String name) {
logger.info("service1 存储userName:" + name);
ThreadLocal<String> holder = MyUserContextHolder.getHolder();
holder.set(name);
MyService2 service2 = new MyService2();
service2.doWork2();
}
}
class MyService2 {
private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);
public void doWork2() {
ThreadLocal<String> holder = MyUserContextHolder.getHolder();
logger.info("service2 获取userName:" + holder.get());
MyService3 service3 = new MyService3();
service3.doWork3();
}
}
class MyService3 {
private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);
public void doWork3() {
ThreadLocal<String> holder = MyUserContextHolder.getHolder();
logger.info("service3获取 userName:" + holder.get());
// 避免oom问题
holder.remove();
}
}
从输出结果来看,在单例对象情况下,既保证了同一个线程间变量共享。
也保证了不同线程之间变量的隔离。
基于源码了解ThreadlLocal工作原理
ThreadlLocal如何做到线程隔离的?
我们下面这段代码为例进行分析,本质上ThreadLocal
的withInitial
指明了每个线程初始化时设置默认值:
ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS"));
当我们执行get操作时,threadLocal
就会为当前线程完成内部map
的初始化,然后通过initialValue
获取上一步声明的SimpleDateFormat
实例,由此保证每个线程内部都有一个独有的SimpleDateFormat
:
对应的我们给出ThreadlLocal
的get
的源码,整体逻辑与上述差不多,即初始化线程内部的map,然后通过setInitialValue
调用initialValue
创建初始值存到线程的map
中:
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//拿到当前线程中的map
ThreadLocalMap map = getMap(t);
//如果map不为空则取用当前这个ThreadLocal作为key取出值,否则通过setInitialValue完成ThreadLocal初始化
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private T setInitialValue() {
//执行initialValue为当前线程创建变量value,在这里也就是我们要用的SimpleDateFormat
T value = initialValue();
//获取当前线程map,有则直接以ThreadLocal为key将SimpleDateFormat 设置进去,若没有先创建再设置
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
//返回SimpleDateFormat
return value;
}
ThreadLocalMap有什么特点?和HashMap有什么区别
我们通过源码查看到这个map
为ThreadLocalMap
,它是由一个个Entry
构成的数组:
private Entry[] table;
并且每个Entry
的key
是弱引用,这就意味着当触发GC时,Entry
的key
也就是ThreadLocal
就会被回收。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
除上面所说,thread
中的map
和hashmap
还有一个不同点就是数据结构,因为threadLocal
的适用场景特殊,所以大部分情况下其内部存储空间不会存储太多元素,所以出于简单的考虑,线程中的map本质上就是一个数组,一旦发生冲突则直接通过线性探测法找到数组中空闲的位置将值存入:
private void set(ThreadLocal<?> key, Object value) {
//......
Entry[] tab = table;
int len = tab.length;
//定位键值对存储的索引位置
int i = key.threadLocalHashCode & (len-1);
//通过线性探测法循环找到空闲位置存入元素
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//......
}
//找到合适的位置将元素存入
tab[i] = new Entry(key, value);
//更新一下容量信息
int sz = ++size;
//......
}
ThreadLocal使用注意事项
内存泄漏问题
我们有下面这样一段web代码,每次请求test0就会像线程池中的线程存一个4M的byte数组:
RestController
public class TestController {
final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(100, 100, 1, TimeUnit.MINUTES,
new LinkedBlockingQueue<>());// 创建线程池,通过线程池,保证创建的线程存活
final static ThreadLocal<Byte[]> localVariable = new ThreadLocal<Byte[]>();// 声明本地变量
@RequestMapping(value = "/test0")
public String test0(HttpServletRequest request) {
poolExecutor.execute(() -> {
Byte[] c = new Byte[4* 1024* 1024];
localVariable.set(c);// 为线程添加变量
});
return "success";
}
}
我们将这个代码打成jar包部署到服务器上并启动
java -jar -Xms100m -Xmx100m # 调整堆内存大小
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof # 表示发生OOM时输出日志文件,指定path为/tmp/heapdump.hprof
-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/tmp/heapTest.log # 打印日志、gc时间以及指定gc日志的路径
demo-0.0.1-SNAPSHOT.jar
只需频繁调用几次,就会输出OutOfMemoryError
Exception in thread "pool-1-thread-5" java.lang.OutOfMemoryError: Java heap space
at com.example.jstackTest.TestController.lambda$test0$0(TestController.java:25)
at com.example.jstackTest.TestController$$Lambda$582/394910033.run(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
问题的根本原因是我们没有及时回收Thread
从ThreadLocal
中得到的变量副本。因为我们的使用的线程是来自线程池中,所以线程使用结束后并不会被销毁,这就使得ThreadLocal
中的变量副本会一直存储与线程池中的线程中,导致OOM
。
可能你会问了,不是说Java
有GC
回收机制嘛?为什么还会出现Thread
中的ThreadLocalMap
的value
不会被回收呢?
我们上文提到ThreadLocal
得到值,都会以ThreadLocal
为key
,ThreadLocal
的initialValue
方法得到的value
作为值生成一个entry
对象,存到当前线程的ThreadLocalMap
中。 而我们的Entry
的key
是一个弱引用,一旦我们使用的threadLocal
临时变量用完被垃圾回收之后,这个key
就会因为弱引用的原因被回收,而我们这个key
所对应的value
仍然被线程池中的线程的强引用引用着,所以就迟迟无法回收,随着时间推移每个线程都出现这种情况导致OOM
。
所以我们每个线程使用完ThreadLocal
之后,一定要使用remove
方法清楚ThreadLocalMap
中的value:
localVariable.remove()
从源码中可以看到remove
方法会遍历当前线程map
然后将强引用之间的联系切断,确保下次GC
可以回收掉可以无用对象。
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//定位,并将entry清除
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
空指针问题
使用ThreadLocal
存放包装类的时候也需要注意添加初始化方法,否则在拆箱时可能会出现空指针问题。
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Long num = threadLocal.get();
long sum=1+num;
}
输出错误:
Exception in thread "main" java.lang.NullPointerException
at com.guide.base.MyThreadLocalNpe.main(MyThreadLocalNpe.java:11)
解决方式
private static ThreadLocal<Long> threadLocal = ThreadLocal.withInitial(()->new Long(0));
线程重用问题
这个问题和OOM
问题类似,在线程池中服用同一个线程未及时清理,导致下一次HTTP请求时得到上一次ThreadLocal
存储的结果。
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> null);
* 线程池中使用threadLocal示例
*
* @param accountCode
* @return
*/
@GetMapping("/account/getAccountByCode/{accountCode}")
@SentinelResource(value = "getAccountByCode")
ResultData<Map<String, Object>> getAccountByCode(@PathVariable(value = "accountCode") String accountCode) throws InterruptedException {
Map<String, Object> result = new HashMap<>();
CountDownLatch countDownLatch = new CountDownLatch(1);
threadPool.submit(() -> {
String before = Thread.currentThread().getName() + ":" + threadLocal.get();
log.info("before:" + before);
result.put("before", before);
log.info("调用getByCode,请求参数:{}", accountCode);
QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("account_code", accountCode);
Account account = accountService.getOne(queryWrapper);
String after = Thread.currentThread().getName() + ":" + account.getAccountName();
result.put("after", account.getAccountName());
log.info("after:" + after);
threadLocal.set(account.getAccountName());
//完成计算后,使用countDown按下倒计时门闩,通知主线程可以执行后续步骤
countDownLatch.countDown();
});
//等待上述线程池完成
countDownLatch.await();
return ResultData.success(result);
}
从输出结果可以看出,我们第二次进行HTTP请求时,threadLocal第一get获得了上一次请求的值,出现脏数据。
C:Usersxxx>curl http://localhost:9000/account/getAccountByCode/demoData
{"status":100,"message":"操作成功","data":{"before":"pool-2-thread-1:null","after":"pool-2-thread-1:demoData"},"success":true,"timestamp":1678410699943}
C:Usersxxx>curl http://localhost:9000/account/getAccountByCode/Zsy
{"status":100,"message":"操作成功","data":{"before":"pool-2-thread-1:demoData","after":"pool-2-thread-1:zsy"},"success":true,"timestamp":1678410707473}
解决方法也很简单,手动添加一个threadLocal的remove方法即可
@GetMapping("/account/getAccountByCode/{accountCode}")
@SentinelResource(value = "getAccountByCode")
ResultData<Map<String, Object>> getAccountByCode(@PathVariable(value = "accountCode") String accountCode) throws InterruptedException {
Map<String, Object> result = new HashMap<>();
CountDownLatch countDownLatch = new CountDownLatch(1);
try {
threadPool.submit(() -> {
String before = Thread.currentThread().getName() + ":" + threadLocal.get();
log.info("before:" + before);
result.put("before", before);
log.info("调用getByCode,请求参数:{}", accountCode);
QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("account_code", accountCode);
Account account = accountService.getOne(queryWrapper);
String after = Thread.currentThread().getName() + ":" + account.getAccountName();
result.put("after", after);
log.info("after:" + after);
threadLocal.set(account.getAccountName());
//完成计算后,使用countDown按下倒计时门闩,通知主线程可以执行后续步骤
countDownLatch.countDown();
});
} finally {
threadLocal.remove();
}
//等待上述线程池完成
countDownLatch.await();
return ResultData.success(result);
}
ThreadLocal的不可继承性
通过代码证明ThreadLocal的不可继承性
如下代码所示,ThreadLocal
子线程无法拿到主线程维护的内部变量
/**
* ThreadLocal 不具备可继承性
*/
public class ThreadLocalInheritTest {
private static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
private static Logger logger = LoggerFactory.getLogger(ThreadLocalInheritTest.class);
public static void main(String[] args) {
THREAD_LOCAL.set("mainVal");
logger.info("主线程的值为: " + THREAD_LOCAL.get());
new Thread(() -> {
try {
//睡眠3s确保上述逻辑运行
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("子线程获取THREAD_LOCAL的值为:[{}]", THREAD_LOCAL.get());
}).start();
}
}
使用InheritableThreadLocal实现主线程内部变量继承
如下所示,我们将THREAD_LOCAL
改为InheritableThreadLocal
类即可解决问题。
/**
* ThreadLocal 不具备可继承性
*/
public class ThreadLocalInheritTest {
private static ThreadLocal<String> THREAD_LOCAL = new InheritableThreadLocal<>();
private static Logger logger = LoggerFactory.getLogger(ThreadLocalInheritTest.class);
public static void main(String[] args) {
THREAD_LOCAL.set("mainVal");
logger.info("主线程的值为: " + THREAD_LOCAL.get());
new Thread(() -> {
try {
//睡眠3s确保上述逻辑运行
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("子线程获取THREAD_LOCAL的值为:[{}]", THREAD_LOCAL.get());
}).start();
}
}
基于源码剖析原因
因为 ThreadLocal
会将变量存储在线程的 ThreadLocalMap
中,所以我们先看看InheritableThreadLocal
的getMap
方法,从而定位到了inheritableThreadLocals
:
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
然后我们到Thread类去定位这个变量的使用之处,所以我们在创建线程的地方打了个断点:
从而定位到这段初始化,它会获取主线程的ThreadLocalMap
并将主线程ThreadLocalMap
中的值存到子线程的ThreadLocalMap
中。
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
//获取当前线程的主线程
Thread parent = currentThread();
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
//将主线程的map的值存到子线程中
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//......
}
createInheritedMap
内部就会调用ThreadLocalMap
方法将主线程的ThreadLocalMap
的值存到子线程的ThreadLocalMap
中。
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
//遍历父线程数据复制到子线程map中
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
//......
//定位当前子线程bucket位置将value存入
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
ThreadLocal在Spring中的运用
其实针对日期格式化问题,Spring已经为我们内置好了相应的工具类即DateTimeContextHolder
:
private static final ThreadLocal<DateTimeContext> dateTimeContextHolder =
new NamedThreadLocal<>("DateTimeContext");
该工具类和simpledateformate
差不多,使用示例如下所示,是spring
封装的,使用起来也很方便:
public class DateTimeContextHolderTest {
protected static final Logger logger = LoggerFactory.getLogger(DateTimeContextHolderTest.class);
private final static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private Set<String> set = new ConcurrentHashSet<String>();
@Test
public void test_withLocale_same() throws Exception {
ExecutorService threadPool = Executors.newFixedThreadPool(30);
for (int i = 0; i < 30; i++) {
int finalI = i;
threadPool.execute(() -> {
LocalDate currentdate = LocalDate.now();
int year = currentdate.getYear();
int month = currentdate.getMonthValue();
int day = 1 + finalI;
LocalDate date = LocalDate.of(year, month, day);
DateTimeFormatter fmt = DateTimeContextHolder.getFormatter(formatter, null);
String text = date.format(fmt);
set.add(text);
logger.info("转换后的时间为" + text);
});
}
threadPool.shutdown();
while (!threadPool.isTerminated()) {
}
logger.info("查看去重后的数量"+set.size());
}
}
为什么JDK建议将ThreadLocal设置为static
我们都知道使用static
是属于类,存在于方法区中,即修饰的变量是全局共享的,这意味着当前ThreadLocal
在通过static
之后,即所有的实例对象都共享一个ThreadLocal
。从而避免重复创建TSO(Thread Specific Object)
即ThreadLocal
所关联的对象的创建的开销。以及这种方案使得即使出现内存泄漏也是O(1)
级别的内存泄露,场景如下:
- 假设使用线程非线程池模式,即线程结束后
threadLocalMap
就会被回收,这种情况下也只有在threadLocal
第一次调用get到线程销毁之间的时间段存在内存泄漏的情况。 - 如果使用的是全局线程池,因为线程池的线程并不会被回收,所以
threadLocalMap
中的entry
一直存在于堆内存中,但由于该ThreadLocal
属于全局共享,所以大量线程进行操作时一定概率触发expungeStaleEntry
清除过期对象,一定程度上避免了内存泄漏的情况。 - 极端情况下,如果
threadLocal
创建之后只有线程池中的一个线程get或初始化后完全没有线程再去使用,这就会导致threadLocalMap
存在强引用而导致无法被回收,O(1)
级别的内存泄漏由此诞生。
对应的实例变量的ThreadLocal
的O(n)内存泄漏,这就不必多说。
小结
ThreadLocal
通过在将共享变量拷贝一份到每个线程内部的ThreadLocalMap
保证线程安全。ThreadLocal
使用完成后记得使用remove
方法手动清理线程中的ThreadLocalMap
过期对象,避免OOM
和一些业务上的错误。ThreadLocal
是不可被继承了,如果想使用主线的的ThreadLocal
,就必须使用InheritableThreadLocal
。
这篇文章并不代表我对于ThreadLocal
的理解止步于此,笔者也会在后续的文章中不断进行迭代补充,如果你想实时收到这些文章的更新可以通过下方二维码关注一下笔者公众号和笔者保持交流:
参考
Java 并发 – ThreadLocal详解:https://www.pdai.tech/md/java/thread/java-thread-x-threadlocal.html#java-开发手册中推荐的-threadlocal
面试:为了进阿里,死磕了ThreadLocal内存泄露原因:https://www.cnblogs.com/Ccwwlx/p/13581004.html
ThreadLocal出现OOM内存溢出的场景和原理分析:https://www.cnblogs.com/jobbible/p/13364292.html#:~:text=取消注释:threadLocal.remove ();,结果不会出现OOM,可以看出堆内存的变化呈现锯齿状,证明每一次remove ()之后,ThreadLocal的内存释放掉了!
将ThreadLocal变量设置为private static的好处是啥? – Viscent大千的回答 – 知乎 :https://www.zhihu.com/question/35250439/answer/101676937
为了保证特定变量对当前线程可见,我们就可以使用ThreadLocal
关键字,ThreadLocal
可以为每个线程创建这个变量的副本并存到每个线程的存储空间中(关于这个存储空间后文会展开讲述)
,从而确保共享变量对每个线程隔离:
ThreadLocal基础使用示例
如上文所说ThreadLocal最典型的用法就是维护各个线程各自需要独享变量,基于ThreadLocal
为每个将每个线程的id
存到线程内部,彼此之间互不影响。
ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
Thread t1 = new Thread(() -> {
log.info("t1往THREAD_LOCAL存入变量:[{}]", Thread.currentThread().getName());
THREAD_LOCAL.set(Thread.currentThread().getName());
log.info("t1获取THREAD_LOCAL的值为:[{}]", THREAD_LOCAL.get());
}, "t1");
Thread t2 = new Thread(() -> {
log.info("t2往THREAD_LOCAL存入变量:[{}]", Thread.currentThread().getName());
THREAD_LOCAL.set(Thread.currentThread().getName());
log.info("t2获取THREAD_LOCAL的值为:[{}]", THREAD_LOCAL.get());
THREAD_LOCAL.remove();
log.info("t2删除THREAD_LOCAL的后值为:[{}]", THREAD_LOCAL.get());
}, "t2");
t1.start();
t2.start();
ThreadUtil.sleep(1,TimeUnit.DAYS);
从输出结果可以看出,两个线程都用THREAD_LOCAL 在自己的内存空间中存储了变量的副本,彼此互相隔离的使用
21:59:51.351 [t2] INFO MultiApplication - t2往THREAD_LOCAL存入变量:[t2]
21:59:51.351 [t1] INFO MultiApplication - t1往THREAD_LOCAL存入变量:[t1]
21:59:51.358 [t1] INFO MultiApplication - t1获取THREAD_LOCAL的值为:[t1]
21:59:51.359 [t2] INFO MultiApplication - t2获取THREAD_LOCAL的值为:[t2]
21:59:51.359 [t2] INFO MultiApplication - t2删除THREAD_LOCAL的后值为:[null]
从两种应用场景来介绍一下ThreadLocal
日期格式化工具类
我们创建100个线程使用同一个dateFormat
完成日期格式化:
private static Logger logger = LoggerFactory.getLogger(MyThreadLocalDemo3.class);
static SimpleDateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS");
public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(100);
for (int i = 0; i < 100; i++) {
int finalI = i;
//线程池中的线程
threadPool.submit(()->{
new MyThreadLocalDemo3().caclData(finalI);
});
}
threadPool.shutdown();
}
/**
* 计算second后的日期
* @param second
* @return
*/
public String caclData(int second){
Date date=new Date(1000*second);
String dateStr = dateFormat.format(date);
logger.info("{}得到的时间字符串为:{}",Thread.currentThread().getId(),dateStr);
return dateStr;
}
从输出结果可以看出,间隔几毫秒的线程出现相同结果
基于该问题我们使用ThreadLocal为线程分配SimpleDateFormat副本:
static ThreadLocal<SimpleDateFormat> threadLocal=ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS"));
public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(100);
for (int i = 0; i < 100; i++) {
int finalI = i;
//线程池中的线程
threadPool.submit(()->{
new MyThreadLocalDemo3().caclData(finalI);
});
}
threadPool.shutdown();
}
/**
* 计算second后的日期
* @param second
* @return
*/
public String caclData(int second){
Date date=new Date(1000*second);
SimpleDateFormat simpleDateFormat = threadLocal.get();
String dateStr = simpleDateFormat.format(date);
logger.info("{}得到的时间字符串为:{}",Thread.currentThread().getId(),dateStr);
return dateStr;
}
服务间调用的线程变量共享
我们日常web开发都会涉及到各种service
的调用,例如某个controller需要调用完service1
之后再调用service2
。因为我们的controller
和service
都是单例的,所以如果我们希望多线程调用这些controller
和service保证共享变量的隔离,也可以用到ThreadLocal
。
为了实现这个示例,我们编写了线程获取共享变量的工具类:
public class MyUserContextHolder {
private static ThreadLocal<User> holder = new ThreadLocal<>();
public static ThreadLocal<User> getHolder() {
return holder;
}
}
service
调用链示例如下,笔者创建service1
之后,所有线程复用这个service
完成了调用,并且在服务间调用直接通过ThreadLocal
完成了线程副本共享:
public class MyThreadLocalGetUserId {
private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);
private static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
int finalI = i;
MyService1 service1 = new MyService1();
threadPool.submit(() -> {
service1.doWork1("username" + (finalI+1));
});
}
}
}
class MyService1 {
private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);
public void doWork1(String name) {
logger.info("service1 存储userName:" + name);
ThreadLocal<String> holder = MyUserContextHolder.getHolder();
holder.set(name);
MyService2 service2 = new MyService2();
service2.doWork2();
}
}
class MyService2 {
private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);
public void doWork2() {
ThreadLocal<String> holder = MyUserContextHolder.getHolder();
logger.info("service2 获取userName:" + holder.get());
MyService3 service3 = new MyService3();
service3.doWork3();
}
}
class MyService3 {
private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);
public void doWork3() {
ThreadLocal<String> holder = MyUserContextHolder.getHolder();
logger.info("service3获取 userName:" + holder.get());
// 避免oom问题
holder.remove();
}
}
从输出结果来看,在单例对象情况下,既保证了同一个线程间变量共享。
也保证了不同线程之间变量的隔离。
基于源码了解ThreadlLocal工作原理
ThreadlLocal如何做到线程隔离的?
我们下面这段代码为例进行分析,本质上ThreadLocal
的withInitial
指明了每个线程初始化时设置默认值:
ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS"));
当我们执行get操作时,threadLocal
就会为当前线程完成内部map
的初始化,然后通过initialValue
获取上一步声明的SimpleDateFormat
实例,由此保证每个线程内部都有一个独有的SimpleDateFormat
:
对应的我们给出ThreadlLocal
的get
的源码,整体逻辑与上述差不多,即初始化线程内部的map,然后通过setInitialValue
调用initialValue
创建初始值存到线程的map
中:
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//拿到当前线程中的map
ThreadLocalMap map = getMap(t);
//如果map不为空则取用当前这个ThreadLocal作为key取出值,否则通过setInitialValue完成ThreadLocal初始化
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private T setInitialValue() {
//执行initialValue为当前线程创建变量value,在这里也就是我们要用的SimpleDateFormat
T value = initialValue();
//获取当前线程map,有则直接以ThreadLocal为key将SimpleDateFormat 设置进去,若没有先创建再设置
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
//返回SimpleDateFormat
return value;
}
ThreadLocalMap有什么特点?和HashMap有什么区别
我们通过源码查看到这个map
为ThreadLocalMap
,它是由一个个Entry
构成的数组:
private Entry[] table;
并且每个Entry
的key
是弱引用,这就意味着当触发GC时,Entry
的key
也就是ThreadLocal
就会被回收。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
除上面所说,thread
中的map
和hashmap
还有一个不同点就是数据结构,因为threadLocal
的适用场景特殊,所以大部分情况下其内部存储空间不会存储太多元素,所以出于简单的考虑,线程中的map本质上就是一个数组,一旦发生冲突则直接通过线性探测法找到数组中空闲的位置将值存入:
private void set(ThreadLocal<?> key, Object value) {
//......
Entry[] tab = table;
int len = tab.length;
//定位键值对存储的索引位置
int i = key.threadLocalHashCode & (len-1);
//通过线性探测法循环找到空闲位置存入元素
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//......
}
//找到合适的位置将元素存入
tab[i] = new Entry(key, value);
//更新一下容量信息
int sz = ++size;
//......
}
ThreadLocal使用注意事项
内存泄漏问题
我们有下面这样一段web代码,每次请求test0就会像线程池中的线程存一个4M的byte数组:
RestController
public class TestController {
final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(100, 100, 1, TimeUnit.MINUTES,
new LinkedBlockingQueue<>());// 创建线程池,通过线程池,保证创建的线程存活
final static ThreadLocal<Byte[]> localVariable = new ThreadLocal<Byte[]>();// 声明本地变量
@RequestMapping(value = "/test0")
public String test0(HttpServletRequest request) {
poolExecutor.execute(() -> {
Byte[] c = new Byte[4* 1024* 1024];
localVariable.set(c);// 为线程添加变量
});
return "success";
}
}
我们将这个代码打成jar包部署到服务器上并启动
java -jar -Xms100m -Xmx100m # 调整堆内存大小
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof # 表示发生OOM时输出日志文件,指定path为/tmp/heapdump.hprof
-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/tmp/heapTest.log # 打印日志、gc时间以及指定gc日志的路径
demo-0.0.1-SNAPSHOT.jar
只需频繁调用几次,就会输出OutOfMemoryError
Exception in thread "pool-1-thread-5" java.lang.OutOfMemoryError: Java heap space
at com.example.jstackTest.TestController.lambda$test0$0(TestController.java:25)
at com.example.jstackTest.TestController$$Lambda$582/394910033.run(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
问题的根本原因是我们没有及时回收Thread
从ThreadLocal
中得到的变量副本。因为我们的使用的线程是来自线程池中,所以线程使用结束后并不会被销毁,这就使得ThreadLocal
中的变量副本会一直存储与线程池中的线程中,导致OOM
。
可能你会问了,不是说Java
有GC
回收机制嘛?为什么还会出现Thread
中的ThreadLocalMap
的value
不会被回收呢?
我们上文提到ThreadLocal
得到值,都会以ThreadLocal
为key
,ThreadLocal
的initialValue
方法得到的value
作为值生成一个entry
对象,存到当前线程的ThreadLocalMap
中。 而我们的Entry
的key
是一个弱引用,一旦我们使用的threadLocal
临时变量用完被垃圾回收之后,这个key
就会因为弱引用的原因被回收,而我们这个key
所对应的value
仍然被线程池中的线程的强引用引用着,所以就迟迟无法回收,随着时间推移每个线程都出现这种情况导致OOM
。
所以我们每个线程使用完ThreadLocal
之后,一定要使用remove
方法清楚ThreadLocalMap
中的value:
localVariable.remove()
从源码中可以看到remove
方法会遍历当前线程map
然后将强引用之间的联系切断,确保下次GC
可以回收掉可以无用对象。
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//定位,并将entry清除
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
空指针问题
使用ThreadLocal
存放包装类的时候也需要注意添加初始化方法,否则在拆箱时可能会出现空指针问题。
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Long num = threadLocal.get();
long sum=1+num;
}
输出错误:
Exception in thread "main" java.lang.NullPointerException
at com.guide.base.MyThreadLocalNpe.main(MyThreadLocalNpe.java:11)
解决方式
private static ThreadLocal<Long> threadLocal = ThreadLocal.withInitial(()->new Long(0));
线程重用问题
这个问题和OOM
问题类似,在线程池中服用同一个线程未及时清理,导致下一次HTTP请求时得到上一次ThreadLocal
存储的结果。
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> null);
* 线程池中使用threadLocal示例
*
* @param accountCode
* @return
*/
@GetMapping("/account/getAccountByCode/{accountCode}")
@SentinelResource(value = "getAccountByCode")
ResultData<Map<String, Object>> getAccountByCode(@PathVariable(value = "accountCode") String accountCode) throws InterruptedException {
Map<String, Object> result = new HashMap<>();
CountDownLatch countDownLatch = new CountDownLatch(1);
threadPool.submit(() -> {
String before = Thread.currentThread().getName() + ":" + threadLocal.get();
log.info("before:" + before);
result.put("before", before);
log.info("调用getByCode,请求参数:{}", accountCode);
QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("account_code", accountCode);
Account account = accountService.getOne(queryWrapper);
String after = Thread.currentThread().getName() + ":" + account.getAccountName();
result.put("after", account.getAccountName());
log.info("after:" + after);
threadLocal.set(account.getAccountName());
//完成计算后,使用countDown按下倒计时门闩,通知主线程可以执行后续步骤
countDownLatch.countDown();
});
//等待上述线程池完成
countDownLatch.await();
return ResultData.success(result);
}
从输出结果可以看出,我们第二次进行HTTP请求时,threadLocal第一get获得了上一次请求的值,出现脏数据。
C:Usersxxx>curl http://localhost:9000/account/getAccountByCode/demoData
{"status":100,"message":"操作成功","data":{"before":"pool-2-thread-1:null","after":"pool-2-thread-1:demoData"},"success":true,"timestamp":1678410699943}
C:Usersxxx>curl http://localhost:9000/account/getAccountByCode/Zsy
{"status":100,"message":"操作成功","data":{"before":"pool-2-thread-1:demoData","after":"pool-2-thread-1:zsy"},"success":true,"timestamp":1678410707473}
解决方法也很简单,手动添加一个threadLocal的remove方法即可
@GetMapping("/account/getAccountByCode/{accountCode}")
@SentinelResource(value = "getAccountByCode")
ResultData<Map<String, Object>> getAccountByCode(@PathVariable(value = "accountCode") String accountCode) throws InterruptedException {
Map<String, Object> result = new HashMap<>();
CountDownLatch countDownLatch = new CountDownLatch(1);
try {
threadPool.submit(() -> {
String before = Thread.currentThread().getName() + ":" + threadLocal.get();
log.info("before:" + before);
result.put("before", before);
log.info("调用getByCode,请求参数:{}", accountCode);
QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("account_code", accountCode);
Account account = accountService.getOne(queryWrapper);
String after = Thread.currentThread().getName() + ":" + account.getAccountName();
result.put("after", after);
log.info("after:" + after);
threadLocal.set(account.getAccountName());
//完成计算后,使用countDown按下倒计时门闩,通知主线程可以执行后续步骤
countDownLatch.countDown();
});
} finally {
threadLocal.remove();
}
//等待上述线程池完成
countDownLatch.await();
return ResultData.success(result);
}
ThreadLocal的不可继承性
通过代码证明ThreadLocal的不可继承性
如下代码所示,ThreadLocal
子线程无法拿到主线程维护的内部变量
/**
* ThreadLocal 不具备可继承性
*/
public class ThreadLocalInheritTest {
private static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
private static Logger logger = LoggerFactory.getLogger(ThreadLocalInheritTest.class);
public static void main(String[] args) {
THREAD_LOCAL.set("mainVal");
logger.info("主线程的值为: " + THREAD_LOCAL.get());
new Thread(() -> {
try {
//睡眠3s确保上述逻辑运行
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("子线程获取THREAD_LOCAL的值为:[{}]", THREAD_LOCAL.get());
}).start();
}
}
使用InheritableThreadLocal实现主线程内部变量继承
如下所示,我们将THREAD_LOCAL
改为InheritableThreadLocal
类即可解决问题。
/**
* ThreadLocal 不具备可继承性
*/
public class ThreadLocalInheritTest {
private static ThreadLocal<String> THREAD_LOCAL = new InheritableThreadLocal<>();
private static Logger logger = LoggerFactory.getLogger(ThreadLocalInheritTest.class);
public static void main(String[] args) {
THREAD_LOCAL.set("mainVal");
logger.info("主线程的值为: " + THREAD_LOCAL.get());
new Thread(() -> {
try {
//睡眠3s确保上述逻辑运行
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("子线程获取THREAD_LOCAL的值为:[{}]", THREAD_LOCAL.get());
}).start();
}
}
基于源码剖析原因
因为 ThreadLocal
会将变量存储在线程的 ThreadLocalMap
中,所以我们先看看InheritableThreadLocal
的getMap
方法,从而定位到了inheritableThreadLocals
:
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
然后我们到Thread类去定位这个变量的使用之处,所以我们在创建线程的地方打了个断点:
从而定位到这段初始化,它会获取主线程的ThreadLocalMap
并将主线程ThreadLocalMap
中的值存到子线程的ThreadLocalMap
中。
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
//获取当前线程的主线程
Thread parent = currentThread();
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
//将主线程的map的值存到子线程中
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//......
}
createInheritedMap
内部就会调用ThreadLocalMap
方法将主线程的ThreadLocalMap
的值存到子线程的ThreadLocalMap
中。
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
//遍历父线程数据复制到子线程map中
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
//......
//定位当前子线程bucket位置将value存入
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
ThreadLocal在Spring中的运用
其实针对日期格式化问题,Spring已经为我们内置好了相应的工具类即DateTimeContextHolder
:
private static final ThreadLocal<DateTimeContext> dateTimeContextHolder =
new NamedThreadLocal<>("DateTimeContext");
该工具类和simpledateformate
差不多,使用示例如下所示,是spring
封装的,使用起来也很方便:
public class DateTimeContextHolderTest {
protected static final Logger logger = LoggerFactory.getLogger(DateTimeContextHolderTest.class);
private final static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private Set<String> set = new ConcurrentHashSet<String>();
@Test
public void test_withLocale_same() throws Exception {
ExecutorService threadPool = Executors.newFixedThreadPool(30);
for (int i = 0; i < 30; i++) {
int finalI = i;
threadPool.execute(() -> {
LocalDate currentdate = LocalDate.now();
int year = currentdate.getYear();
int month = currentdate.getMonthValue();
int day = 1 + finalI;
LocalDate date = LocalDate.of(year, month, day);
DateTimeFormatter fmt = DateTimeContextHolder.getFormatter(formatter, null);
String text = date.format(fmt);
set.add(text);
logger.info("转换后的时间为" + text);
});
}
threadPool.shutdown();
while (!threadPool.isTerminated()) {
}
logger.info("查看去重后的数量"+set.size());
}
}
为什么JDK建议将ThreadLocal设置为static
我们都知道使用static
是属于类,存在于方法区中,即修饰的变量是全局共享的,这意味着当前ThreadLocal
在通过static
之后,即所有的实例对象都共享一个ThreadLocal
。从而避免重复创建TSO(Thread Specific Object)
即ThreadLocal
所关联的对象的创建的开销。以及这种方案使得即使出现内存泄漏也是O(1)
级别的内存泄露,场景如下:
-
假设使用线程非线程池模式,即线程结束后
threadLocalMap
就会被回收,这种情况下也只有在threadLocal
第一次调用get到线程销毁之间的时间段存在内存泄漏的情况。 -
如果使用的是全局线程池,因为线程池的线程并不会被回收,所以
threadLocalMap
中的entry
一直存在于堆内存中,但由于该ThreadLocal
属于全局共享,所以大量线程进行操作时一定概率触发expungeStaleEntry
清除过期对象,一定程度上避免了内存泄漏的情况。 -
极端情况下,如果
threadLocal
创建之后只有线程池中的一个线程get或初始化后完全没有线程再去使用,这就会导致threadLocalMap
存在强引用而导致无法被回收,O(1)
级别的内存泄漏由此诞生。
对应的实例变量的ThreadLocal
的O(n)内存泄漏,这就不必多说。
小结
-
ThreadLocal
通过在将共享变量拷贝一份到每个线程内部的ThreadLocalMap
保证线程安全。 -
ThreadLocal
使用完成后记得使用remove
方法手动清理线程中的ThreadLocalMap
过期对象,避免OOM
和一些业务上的错误。 -
ThreadLocal
是不可被继承了,如果想使用主线的的ThreadLocal
,就必须使用InheritableThreadLocal
。
这篇文章并不代表我对于ThreadLocal
的理解止步于此,笔者也会在后续的文章中不断进行迭代补充,如果你想实时收到这些文章的更新可以通过下方二维码关注一下笔者公众号和笔者保持交流:
参考
Java 并发 – ThreadLocal详解:https://www.pdai.tech/md/java/thread/java-thread-x-threadlocal.html#java-开发手册中推荐的-threadlocal
面试:为了进阿里,死磕了ThreadLocal内存泄露原因:https://www.cnblogs.com/Ccwwlx/p/13581004.html
ThreadLocal出现OOM内存溢出的场景和原理分析:https://www.cnblogs.com/jobbible/p/13364292.html#:~:text=取消注释:threadLocal.remove ();,结果不会出现OOM,可以看出堆内存的变化呈现锯齿状,证明每一次remove ()之后,ThreadLocal的内存释放掉了!
将ThreadLocal变量设置为private static的好处是啥? – Viscent大千的回答 – 知乎 :https://www.zhihu.com/question/35250439/answer/101676937