首页 > 其他分享 >谈谈对TransmittableThreadLocal的理解

谈谈对TransmittableThreadLocal的理解

时间:2023-08-14 09:44:59浏览次数:37  
标签:get ThreadLocal value 谈谈 理解 线程 TransmittableThreadLocal TTL TtlRunnable

前言

最近遇到一个问题,公司内部有一个公共的SSO包,用来获取HTTP请求中的登录态,代码中会直接用这个包的方法获取用户登录信息,在代码任意位置直接用SSOUtil.getUser()获取用户信息,在我们一个下载的业务代码中,用到了线程池开启子任务处理请求,结果发现子任务中拿到的用户信息和HTTP请求主线程中的不一致,导致了一些业务问题。

出问题的交互流程如下:

最后在SSO包的源码中排查到了问题所在,SSO包用到了一个TransmittableThreadLocal(本文统一简称TTL)来存储用户信息到当前线程。本文的目的是探究一下TTL的原理,在这之前会先回顾一下ThreadLocal和InheritableThreadLocal的实现原理。

ThreadLocal

ThreadLocal是Java的一个类,顾名思义,“线程本地”变量,用于保存线程私有的变量。ThreadLocal有一个内部类叫ThreadLocalMap,ThreadLocalMap底层是数组,数组中存放多个Entry对象,这个对象的Key是一个指向ThreadLocal变量的WeakReference,Value是当前线程该ThreadLocal变量的值。对于每一个Java线程,在JVM中对应一个Thread对象,每个Thread对象里面持有一个ThreadLocalMap,它们之间的关系用下图表示更加清楚。

ThreadLocal的核心类ThreadLocalMap部分源码如下

static class ThreadLocalMap {

/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

private static final int INITIAL_CAPACITY = 16;

/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;

/**
* The number of entries in the table.
*/
private int size = 0;

/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0


}

InheritableThreadLocal

InheritableThreadLocal也是Java的一个类,它是ThreadLocal的子类,它的出现是为了解决ThreadLocal在线程间传递过程中丢失的问题,上面我们知道Thread中维护了一个Map,用来存放当前线程本地变量的值,但是开启新线程,这个变量就失效了。Thread类还有另一个ThreadLocalMap即inheritableThreadLocals,下面是Thread.init()方法中的一段代码,当线程创建时,会从主线程复制inheritableThreadLocals到子线程,完成父子线程本地变量的传递。

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;

ITL的实现方法也非常巧妙,它继承了ThreadLocal,重写了getMap方法,在ThreadLocal设置值的时候,会通过getMap来拿到ThreadLocalMap,通过重写拿到了Thread类中的inheritableThreadLocals,从而实现了把ITL的值都通过另一个Map来存放。

//InheritableThreadLocal代码
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}

//ThreadLocal的set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

TransmittableThreadLocal

ITL类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把任务提交给线程池时的ThreadLocal值传递到任务执行时。

TransmittableThreadLocal(TTL)的出现是解决这个问题的,TTL是Alibaba开源的一个类,TTL继承了ITL,使用方式也类似。相比InheritableThreadLocal,添加了copy方法用于定制任务提交给线程池时的ThreadLocal值传递到任务执行时的拷贝行为,缺省传递的是引用。注意:如果跨线程传递了对象引用因为不再有线程封闭,与InheritableThreadLocal.childValue一样,使用者/业务逻辑要注意传递对象的线程安全。

它使用TtlRunnable和TtlCallable来修饰传入线程池的Runnable和Callable。

示例代码:

TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();

// =====================================================

// 在父线程中设置
context.set("value-set-in-parent");

Runnable task = new RunnableTask();
// 额外的处理,生成修饰了的对象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(task);
executorService.submit(ttlRunnable);

// =====================================================

// Task中可以读取,值是"value-set-in-parent"
String value = context.get();

注意:即使是同一个Runnable任务多次提交到线程池时,每次提交时都需要通过修饰操作(即TtlRunnable.get(task))以抓取这次提交时的TransmittableThreadLocal上下文的值;即如果同一个任务下一次提交时不执行修饰而仍然使用上一次的TtlRunnable,则提交的任务运行时会是之前修饰操作所抓取的上下文。示例代码如下:

// 第一次提交
Runnable task = new RunnableTask();
executorService.submit(TtlRunnable.get(task));

// ...业务逻辑代码,
// 并且修改了 TransmittableThreadLocal上下文 ...
// context.set("value-modified-in-parent");

// 再次提交
// 重新执行修饰,以传递修改了的 TransmittableThreadLocal上下文
executorService.submit(TtlRunnable.get(task));

上述用法对Callable也是类似的。

下面我们通过源码看下TTL是怎么实现池化线程间传递的。看之前可以思考一下,在ITL中,其实是做到了新起子线程时,复制ITL。池化的线程做不到,是因为复用线程场景没有这个触发的时机了,那么TTL一样需要这样的一个触发时机,只不过不是ITL中的Thread.init(),通过上面的用法示例,我们知道这个触发时机实际上就是TtlRunnable.get(),我们可以直接看下get()做了哪些事

//Step1:一层简单包装->调用构造方法创建TtlRunnable
public static TtlRunnable get(@Nullable Runnable runnable, boolean releaseTtlValueReferenceAfterRun, boolean idempotent) {
if (null == runnable) return null;

if (runnable instanceof TtlEnhanced) {
// avoid redundant decoration, and ensure idempotency
if (idempotent) return (TtlRunnable) runnable;
else throw new IllegalStateException("Already TtlRunnable!");
}
return new TtlRunnable(runnable, releaseTtlValueReferenceAfterRun);
}

//Step2:创建对象时capture()方法复制了父线程TTL的值,这个值通过holder来维护(set时会调用addThisToHolder()将TTL值设置进holder)
private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
this.capturedRef = new AtomicReference<Object>(capture());
this.runnable = runnable;
this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}

//Step3:执行Run方法,先对当前线程中的TTL进行备份,然后通过replay方法将父线程的TTL添加进当前线程,最后在finnaly代码中对之前的TTL进行恢复
@Override
public void run() {
final Object captured = capturedRef.get();
if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
throw new IllegalStateException("TTL value reference is released after run!");
}

final Object backup = replay(captured);
try {
runnable.run();
} finally {
restore(backup);
}
}

通过上面的源码可以清楚的了解TTL实现的原理。首先是获得父线程的TTL,然后将子线程的TTLMap进行备份,接着将父线程的TTL循环复制进子线程,最后在子线程执行完runnable.run()以后,将子线程的TTLMap还原。官方推荐的用法中,可以直接包装线程池,原理是类似的,在新线程池的run之前,执行TTLRunnable.get(),这样的用法对业务代码侵入更小,比较推荐。

ExecutorService executorService = ...
// 额外的处理,生成修饰了的对象executorService
executorService = TtlExecutors.getTtlExecutorService(executorService);

回到前言中的问题,SSO的包里面已经引入了TTL,但是我们的只用到了get()、set()方法,其实作用和ITL还是一样的。因此在池化的线程中间,之前设置过用户信息存放在TTL,线程退出后也无法清除,后续其他用户用到了这个线程,获取的还是之前用户的信息,导致了业务异常。

我们可以用TTLRunnable来包装一下业务的Runnable,问题就解决了。

Reference

https://github.com/alibaba/transmittable-thread-local

 

标签:get,ThreadLocal,value,谈谈,理解,线程,TransmittableThreadLocal,TTL,TtlRunnable
From: https://www.cnblogs.com/tiancai/p/17627809.html

相关文章

  • interface理解
    interface(接口)是golang最重要的特性之一,实现多态。Interface类型可以定义一组方法,但是这些不需要实现。并且interface不能包含任何变量。特点interface是方法或行为声明的集合interface接口方式实现比较隐性,任何类型的对象实现interface所包含的全部方法,则表明该类型实现了......
  • 《深入理解Java虚拟机》读书笔记:内存分配策略
    Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。关于回收内存这一点,我们已经使用了大量篇幅去介绍虚拟机中的垃圾收集器体系以及运作原理,现在我们再一起来探讨一下给对象分配内存的那点事儿。对象的内......
  • 理解 Java 方法调用
    总结自:《Java核心技术第10版(套装共2册)-凯S.霍斯特曼霍斯特曼科内尔》下面假设要调用x.f(args),隐式参数x[1]声明为类C的一个引用。下面是调用过程的详细描述:1)编译器查看对象的声明类型和方法名。假设调用x.f(param),且隐式参数x声明为C类的对象。需要注意的是......
  • 深入理解JavaScript正则表达式:释放其强大力量
    深入理解JavaScript正则表达式:释放其强大力量正则表达式是一种强大的工具,用于在字符串中搜索、匹配和替换特定的模式。在JavaScript中,正则表达式是一种内置的功能,可以帮助开发人员处理各种字符串操作。本文将深入探讨JavaScript正则表达式的原理、语法和应用场景,帮助读者充分理解......
  • 深入理解 Spring Bean 的生命周期与初始化过程
    SpringFramework是一个广泛使用的开发框架,它提供了强大的依赖注入和控制反转功能,同时也涉及了丰富的Bean生命周期管理。在本篇博客中,我们将深入探讨SpringBean的生命周期以及初始化过程,并通过代码示例演示每个阶段的实际调用。1.Bean生命周期阶段SpringBean的生命周期可......
  • 《深入理解Java虚拟机》读书笔记:垃圾收集器
    垃圾收集器 HotSpot虚拟机包含的所有收集器如图3-5所示。图3-5展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。新生代收集器:Serial、ParNew、ParallelScavenge,新生代收集器均采用复制算法老年代收集器:SerialOld(标记-整理算法)、Paral......
  • 通俗理解OSI七层模型
    OSI七层模型国际标准化组织(InternationalStandardOrganization,ISO)于1984年颁布了开放系统互连(OpenSystemInterconnection,OSI)参考模型OSI参考模型是一个开放式体系结构,它规定将网络分为七层,从下往上依次是:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层ISO制订的......
  • 从零开始一起学习SLAM | 理解图优化,一步步带你看懂g2o代码
    理解图优化,一步步带你看懂g2o框架小白:师兄师兄,最近我在看SLAM的优化算法,有种方法叫“图优化”,以前学习算法的时候还有一个优化方法叫“凸优化”,这两个不是一个东西吧?师兄:哈哈,这个问题有意思,虽然它们中文发音一样,但是意思差别大着呢!我们来看看英文表达吧,图优化的英文是graphoptimi......
  • Anyline+PostgeSQL使用理解之二
    第一篇:springboot+postgresql集成anyline试水总结几个目前遇到的比较简单的使用场景,以后可能会继续在此更新。下文anylineService皆为org.anyline.service.AnylineService。查询基本列表查询DataSetds=anylineService.querys(TABLE_NAME+"(id,row,col,start_time,senso......
  • JAVA 内存详解 (理解 JVM 如何使用 Windows 和 Linux 上的本机内存)
    级别:中级AndrewHall ,软件工程师,IBM2009年5月11日Java™堆耗尽并不是造成 java.lang.OutOfMemoryError 的惟一原因。如果本机内存 耗尽,则会发生普通调试技巧无法解决的 OutOfMemoryError 。本文将讨论本机内存的概念,Java运行时如何使用它,它被耗......