​Thread Local深度解析,你学会了吗?

开发 前端
如果使用强引用,当​ThreadLocal​ 对象的引用(强引用)被回收了,​ThreadLocalMap​本身依然还持有​ThreadLocal​的强引用,如果没有手动删除这个key ,则​ThreadLocal​不会被回收,所以只要当前线程不消亡,​ThreadLocalMap​引用的那些对象就不会被回收, 可以认为这导致​Entry​内存泄漏。

今天,有个朋友问我说他想在并发条件下统计接口的耗时以及日期,并做一个记录在最后统一保存,这里我就直接想到了ThreadLocal,其实我用ThreadLocal的场景还挺多的,毕竟项目需要,其实一直都想对ThreadLocal做一个总结,择日不如撞日就现在动手吧。

ThreadLocal概念

ThreadLocal也叫做本地线程变量,ThreadLocal中填充的是当前线程的变量,该变量对其他线程是隔离的,ThreadLocal在每个线程中都创建了一个变量副本,所以每个线程中的ThreadLocal都是一个独立的副本,自己可以访问自己线程内部的副本变量互不干扰。

ThreadLocal使用场景

ThreadLocal的使用也要看情况来定,按个人理解ThreadLocal大致会使用到以下场景:

  • 需要全局获取变量(保证这个变量在全局中的一致性)
  • 需要解决线程安全的场景(例如:记录每个请求的一些信息,保存到日志表中)
  • 父子线程需要共享数据(例如:需要子线程的结果回调给父线程,如何保存它的唯一性)

说白了ThreadLocal就是做数据隔离,每条线程的ThreadLocal都是隔离的互不干扰,其实就是为了防止多线程环境下变量被其他线程篡改,只要记住这点在工作中什么场景下会使用到就一目了然了。

实际上Spring就是采用了Threadlocal来实现单个线程中的数据库操作使用的是同一个数据库连接,采用Threadlocal可以使业务层使用事务的时候不需要去管理connection对象,通过传播级别就能管理多个事务配置之间的切换,挂起和恢复。

Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在TransactionSynchronizationManager这个类里面,代码如下所示:

private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);

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");

注意:在Spring5.2以后的版本Spring事务隔离从ThreadLocal换成了Mono响应式编程来实现隔离。

图片图片

ThreadLocal源码分析

图片图片

从源码上看其实ThreadLocal的set方法并不复杂
  1. 获取当前线程对象Thread.currentThread();
  2. 获取线程变量ThreadLocalMap map = getMap(t);
  3. 如果不为空则赋值map.set(this,value);
  4. 如果为空,初始化该线程对象的map变量,其中key为当前的threadlocal变量createMap(t,value);
再看看ThreadLocal的get方法

图片图片

图片图片

  • 返回当前线程变量的副本中的值,如果该变量没有当前线程的值,则先调用initialValue方法的返回值
  • initialValue方法中继续获取当前线程变量(Key为当前线程)而Value设置为null
  • 如果当前线程副本变量为空那么重新创建当前线程的Map(Key为当前线程,Value为null)
ThreadLocal如何做到线程隔离?

上面分析了ThreadLocal的set()和get()源码,在通过get()方法获取当前线程中副本变量为null那么直接创建一个ThreadLocalMap:

图片图片

从这里入手,看一下t.threadLocals。

图片图片

注释说得很清楚:ThreadLocal属于当前这个线程的。

注意:这个ThreadLocalMap是一个静态内部类。

图片图片

ThreadLocalMap is a customized hash map suitable only for maintaining thread local values. No operations are exported outside of the ThreadLocal class. The class is package private to allow declaration of fields in class Thread. To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. However, since reference queues are not used, stale entries are guaranteed to be removed only when the table starts running out of space.

到此为止其实ThreadLocal的数据隔离的真相就出来了,说白了每个线程Thread都维护了自己的一个threadLocals变量,当线程创建ThreadLocal的时候,实际上数据是存在自己的线程Thread的threadLocals变量里面,可以看出来这个ThreadLocalMap这个类只有一份,在线程中,所以实现了线程之间的隔离。

ThreadLocalMap底层原理

图片图片

虽然看着ThreadLocalMap很像是HashMap,实际上并没有实现Map接口,而是它的内部类Entry继承了WeakReference这个弱引用,也就是说不存在链表的关系了。

接下来我们来看一下ThreadLocalMap的set()方法(这里图片没有截全):

图片图片

ThreadLocalMap在存储的时候每次都会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,int i = key.threadLocalHashCode & (len - 1);

接下来判断如果当前位置为null,就初始化一个Entry对象放在位置上。

图片图片

如果当前位置i不为空,又刚好这个Entry对象的key正好是即将设置的key,那么就覆盖Entry中的value。

图片图片

如果位置i不为null并且key不等于 entry,那么就找下一个空位置,直到位置为空为止然后存放。

在get的时候就会根据ThreadLocal对象的Hash值,定位到相应位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下个位置。

如何共享ThreadLocal中的数据?

使用 InheritableThreadLocal可以实现多个线程访问ThreadLocal的值。

问题是它们之间是如何实现传递的?

其实逻辑很简单,继续看Thread的源码,看下初始化的时候Thread.init做了什么操作:

图片图片

如果线程的inheritThreadLocals变量不为空的话,并且父线程的inheritThreadLocals不为空的话,就把线程的inheritThreadLocals给当前线程的inheritThreadLocals。

图片图片

关于ThreadLocal内存泄露

ThreadLocal使用不当也会出现问题:那就是内存泄露。

继续查看最开始存储数据的Entry类的源码:

图片图片

其实文档已经说得很直白了:

Note that null keys (i.e. entry.get()* == null 如果 key threadlocal 为 null 了,这个 entry 就可以清除了。

ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收 。

造成内存泄露的原因在于ThreadLocal为null,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap(thread 的内部属性)生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。

再详细点来说,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。

按照道理一个线程使用完,ThreadLocalMap是应该要被清空的,但是现在线程被复用了。

解决办法:

每次在使用完ThreadLocal的时候一定要remove。

为什么ThreadLocal要使用弱引用?

如果使用强引用,当ThreadLocal 对象的引用(强引用)被回收了,ThreadLocalMap本身依然还持有ThreadLocal的强引用,如果没有手动删除这个key ,则ThreadLocal不会被回收,所以只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收, 可以认为这导致Entry内存泄漏。

  • 强引用:普通的引用,强引用指向的对象不会被回收。
  • 软引用:仅有软引用指向的对象,只有发生gc且内存不足,才会被回收。
  • 弱引用:仅有弱引用指向的对象,只要发生gc就会被回收。
责任编辑:武晓燕 来源: Westrice
相关推荐

2022-12-22 08:14:54

2023-06-26 13:08:52

GraphQL服务数据

2023-08-01 12:51:18

WebGPT机器学习模型

2024-01-02 12:05:26

Java并发编程

2023-12-27 07:31:45

json产品场景

2022-07-26 00:25:57

PandasQuery索引器

2023-10-06 14:49:21

SentinelHystrixtimeout

2023-05-05 06:54:07

MySQL数据查询

2022-07-13 08:16:49

RocketMQRPC日志

2022-12-06 07:53:33

MySQL索引B+树

2023-01-31 08:02:18

2022-06-16 07:50:35

数据结构链表

2023-08-26 21:34:28

Spring源码自定义

2024-02-02 11:03:11

React数据Ref

2023-07-30 22:29:51

BDDMockitoAssert测试

2023-03-26 22:31:29

2024-01-19 08:25:38

死锁Java通信

2024-02-04 00:00:00

Effect数据组件

2023-07-26 13:11:21

ChatGPT平台工具

2023-01-10 08:43:15

定义DDD架构
点赞
收藏

51CTO技术栈公众号