一. ThreadLocal
ThreadLocal在并发编程中比较常用,在诸多中间件的源码中都能看到它的身影。 对于ThreadLocal的说明先来一段官方解释:
ThreadLocal提供的是一种线程局部变量。这些变量不同于其它变量的点在于每个线程在获取变量的时候,都拥有它自己相对独立的变量副本。ThreadLocal 的实例一般是私有静态的,可以做到与一个线程绑定某一种状态。
根据官方的阐述可知ThreadLocal设计思路:线程隔离 。将变量缓存在当前线程的内部,只对本线程可见(与线程对象Thread绑定)对其他线程是隔离的。
总结起来一句话:没有共享 就没有伤害
1. 实现原理
在探讨一项技术或者学习一项新技术时最简单的方式就是从一个简单的使用示例入手
public class ThreadLocalDemo {
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal<>();
//添加数据
threadLocal.set("内部变量");
//do something
System.out.println(threadLocal.get());
}
}
上面是ThreadLocal所使用的简单示例,对于不了解ThreadLocal的同学可能会有疑问。前文中说到ThreadLocal是和线程绑定的,但是在使用示例中并没有关于ThreadLocal和Thread的关系。
答案在于ThreadLocal的内部对象ThreadLocalMap。
Threrad类声明的ThreadLocalMap变量:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
- InheritableThreadLocal values pertaining to this thread. This map is
maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
ThreadLocal.set源码:
/**
* 为当前 ThreadLocal 对象关联 value 值
* @param value 要存储在此线程的线程副本的值
*/
public void set(T value) {
// 返回当前 ThreadLocal 所在的线程
Thread t = Thread.currentThread();
// 返回当前线程持有的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// ThreadLocalMap 不为空,则直接存储键值对
map.set(this, value);
} else {
// 否则,创建ThreadLocalMap并存储value
createMap(t, value);
}
}
ThreadLocal的set/get/remove方法都是对ThreadLocalMap的处理
ThreadLocalMap内部结构
下面我们继续探究ThreadLocalMap还是从源码开始研究:
static class ThreadLocalMap {
/** * 键值对实体的存储结构 */ static class Entry extends WeakReference<threadlocal<?>> { /** * 当前线程关联的 value,这个 value 并没有用弱引用追踪 */ Object value; /** * 构造键值对 * * @param k k 作 key,作为 key 的 ThreadLocal 会被包装为一个弱引用 * @param v v 作 value */ Entry(ThreadLocal<!--?--> k, Object v) { super(k); value = v; } } 。。。
}
</threadlocal<?>
在代码中看到了Map、看到了Entry,很容易让我们想到熟悉的HashMap。其实ThreadLocalMap它也是一个KV结构的,不同于HashMap它使用的解决哈希冲突的方式是开放寻址法。另外它的Key继承了WeakReference代表是一个弱引用。
本文对于ThreadLocalMap的解决哈希冲突和扩容逻辑等不展开讨论,只介绍实践应用中的常见问题。后续如果有时间可以单独开设源码文章。
看到这里先捋清各自的关系,看下对象间的引用链路:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。
面试题:ThreadLocal为什么会出现内存泄漏?
想必这个面试题在大家提到ThreadLocal时很常见,搞清楚这个问题需要先讲清楚弱引用的作用。
弱引用是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
在实际项目中我们都会使用线程池来管理线程,对于核心线程一般情况下都是循环利用。也就是说核心线程很大程度上是伴随着整个项目生命周期的。Doug Lea为了照顾我们这些糊涂程序员,为了避免无限制的向ThreadLocalMap内填充数据而不进行删除所导致的内存泄漏问题。对ThreadLocal多了一些特殊的设计。
由上图可以看到ThreadLocal对象被ThreadLocal变量和Entry的key所引用。要想对ThreadLocal对象进行垃圾回收需要断开ThreadLocal变量和Entry的key对其的引用。首先在ThreadLocal变量使用结束后就会失去对ThreadLocal对象的引用。
而将Entry的key设置成弱引用。为了在发生GC后Entry的key就会失去对ThreadLocal对象的引用。最终ThreadLocal对象没有了其他引用在下一次GC时就可以被回收掉了。
但是如果Entry的key不是弱引用而是强引用,即便ThreadLocal变量使用结束失去对ThreadLocal对象的引用。ThreadLocal对象也无法回收掉因为它还会被Entry的Key所引用。这就是Entry的key设置成弱引用的原因。
解决了Key的问题,还需要进一步处理value。Entry的key置为空后便无法对Entry的value进行取值了,如果value不能被清理还是会出现内存泄漏。对于这种场景Doug Lea的设计思路是在其他ThreadLocal使用set/get/remove方法对ThreadLocalMap进行操作时会清理掉ThreadLocalMap中Entry的key为null的值。
下面以ThreadLocalMap的getEntry方法为例:
/** * 返回 key 关联的键值对实体 * * @param key threadLocal * @return */ private Entry getEntry(ThreadLocal key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; // 若 e 不为空,并且 e 的 ThreadLocal 的内存地址和 key 相同,直接返回 if (e != null && e.get() == key) { return e; } else { // 从 i 开始向后遍历找到键值对实体 return getEntryAfterMiss(key, i, e); } }
private Entry getEntryAfterMiss(ThreadLocal<!--?--> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<!--?--> k = e.get(); if (k == key) { return e; } // 遇到key为null if (k == null) { // 从索引 i 开始清理key为null的值 // 将Entry的value设置为null,防止内存泄漏 expungeStaleEntry(i); } else { i = nextIndex(i, len); } e = tab[i]; } return null; }
至此即便使用了线程池中的循环应用线程池、即便我们忘记了调用ThreadLocal的remove方法。Doug Lea帮我们减少了内存泄漏问题的发生。
/** * 使用1个线程的线程池 * 确保两次调用都是同一线程处理 */ private final ExecutorService executorService = Executors.newSingleThreadExecutor();
@GetMapping("/setOne") public void handleOne() { executorService.execute(() -> { VersionThreadLocal threadLocalOne = new VersionThreadLocal(); threadLocalOne.set(new VersionThreadLocalValue()); //do something //清除VersionThreadLocal System.gc(); }); } @GetMapping("/setTwo") public void handleTwo() { executorService.execute(() -> { VersionThreadLocal threadLocalTwo = new VersionThreadLocal(); threadLocalTwo.get(); //do something //清除Entry的value 也就是VersionThreadLocalValue System.gc(); }); }
//新建自定义ThreadLocal,主要是方便堆栈工具中查看类
public class VersionThreadLocal extends ThreadLocal{
}
调用第一个接口可以看到VersionThreadLocal已经被垃圾回收,但是VersionThreadLocalValue对象还存在。
调用第二个接口因为执行了其他ThreadLocal对 ThreadLocalMap的操作,所以把Entry的Key为null的Value值也设置为null。GC后也就是对VersionThreadLocalValue进行了垃圾回收。
虽然ThreadLocal设置的已经很巧妙了,但是如果我们没有进行第二步的操作。那么VersionThreadLocalValue对象会一直存在内存中,造成了内存泄漏。虽然这种情况的内存泄漏对我们的系统来讲不会引发严重问题,但是我们还是需要去避免。
规范做法:
public void handleOne() { executorService.execute(() -> { VersionThreadLocal threadLocalOne = new VersionThreadLocal(); try{ threadLocalOne.set(new VersionThreadLocalValue()); //do something //清除VersionThreadLocal System.gc(); }catch (Exception e){
}finally { //主动执行remove方法 threadLocalOne.remove(); } });
}
2. ThreadLocal在源码中的应用
XXL-Job分布式定时任务中间件使用ThreadLocal设置统一日期转换格式
/** * date util * * @author xuxueli 2018-08-19 01:24:11 */ public class DateUtil {
... private static final ThreadLocal<map<string, dateformat="">> dateFormatThreadLocal = new ThreadLocal<map<string, dateformat="">>(); private static DateFormat getDateFormat(String pattern) { if (pattern==null || pattern.trim().length()==0) { throw new IllegalArgumentException("pattern cannot be empty."); } Map<string, dateformat=""> dateFormatMap = dateFormatThreadLocal.get(); if(dateFormatMap!=null && dateFormatMap.containsKey(pattern)){ return dateFormatMap.get(pattern); } synchronized (dateFormatThreadLocal) { if (dateFormatMap == null) { dateFormatMap = new HashMap<string, dateformat="">(); } dateFormatMap.put(pattern, new SimpleDateFormat(pattern)); dateFormatThreadLocal.set(dateFormatMap); } return dateFormatMap.get(pattern); } ...
}
</string,></string,></map<string,></map<string,>
Spring框架对于ThreadLocal的使用示例
/** * {@link ThreadLocal} subclass that exposes a specified name * as {@link #toString()} result (allowing for introspection). * * @author Juergen Hoeller * @since 2.5.2 * @param the value type * @see NamedInheritableThreadLocal */ public class NamedThreadLocal extends ThreadLocal {
private final String name;
/**
- Create a new NamedThreadLocal with the given name.
- @param name a descriptive name for this ThreadLocal
*/
public NamedThreadLocal(String name) {
Assert.hasText(name, “Name must not be empty”);
this.name = name;
}@Override
public String toString() {
return this.name;
}
}
二. InheritableThreadLocal
ThreadLcoal只可以使用在本线程内,不能处理父线程向子线程传递数据的场景。
private static ThreadLocal threadLocal = new ThreadLocal<>();
public static void main(String args) {
threadLocal.set(“传递变量”);
Thread thread = new Thread(() -> {
System.out.println(“获取变量 :” + threadLocal.get());
});
thread.start();
}
获取变量 :null
需要解决父线程向子线程传递数据的问题,需要使用InheritableThreadLocal
private static InheritableThreadLocal threadLocal = new InheritableThreadLocal<>();
public static void main(String args) {
threadLocal.set(“传递变量”);
Thread thread = new Thread(() -> {
System.out.println(“获取变量 :” + threadLocal.get());
});
thread.start();
}
获取变量 :传递变量
1. 实现原理
InheritableThreadLocal同样是在Thread类里面声明的和ThreadLocal大致相同
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
- InheritableThreadLocal values pertaining to this thread. This map is
maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
/**
* 创建允许子线程继承的 ThreadLocal
* @see ThreadLocal
*/
public class InheritableThreadLocal extends ThreadLocal {
/**
* 拿到父线程的值后,可以在这里处理后再返回给子线程
*
* @param parentValue the parent thread's value
* @return the child thread's initial value
*/
protected T childValue(T parentValue) {
return parentValue;
}
/**
* 获取当前线程内的 inheritableThreadLocals 属性
*
* @param t 当前线程
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
...
}
查看源码可知InheritableThreadLocal继承了ThreadLocal方法,但是对ThreadLcoal的一些核心方法并没有重写。那它又是如何实现父线程向子线程做数据传递呢,答案在Thread中。在每次新创建线程Thread时会执行私有方法init。
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(); /* 忽略无关代码 */ // 当父线程的 inheritableThreadLocals 的值不为空时 // 会把 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; // 线程 id 自增 tid = nextThreadID();
}
在每次新建线程时父线程的InheritableThreadLocal会传递给子线程的InheritableThreadLocal。也就是把父线程的ThreadLocalMap传递给子线程。
2. 总结
由此可总结实现父线程向子线程传递需要两个步骤
- 父线程的InheritableThreadLocal不为空
- 子线程是新建线程
三. TransmittableThreadLocal
在实际项目中很少会频繁创建线程对象,一般会使用线程池。很大程度上线程池内的线程都是复用的不会频繁新建。这种场景对于InheritableThreadLocal是不满足使用条件的。
InheritableThreadLocal生效场景 /** * 使用1个线程的线程池 * 确保两次调用都是同一线程处理 */ private static ExecutorService executorService = Executors.newSingleThreadExecutor(); private static InheritableThreadLocal threadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) throws InterruptedException { threadLocal.set("传递变量"); executorService.execute(()->{ System.out.println("executorService创建线程"); }); //线程休眠 Thread.sleep(100); executorService.execute(()->{ System.out.println("获取变量 :"+threadLocal.get());; }); } --- executorService创建线程 获取变量 :传递变量
InheritableThreadLocal不生效场景
/**
* 使用1个线程的线程池
* 确保两次调用都是同一线程处理
*/
private static ExecutorService executorService = Executors.newSingleThreadExecutor();
private static InheritableThreadLocal threadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
executorService.execute(()->{
System.out.println("executorService创建线程");
});
//线程休眠
Thread.sleep(100);
threadLocal.set("传递变量");
executorService.execute(()->{
System.out.println("获取变量 :"+threadLocal.get());;
});
}
---
executorService创建线程
获取变量 :null
两个场景的区别在于threadLocal.set("传递变量");的位置不同。生效场景InheritableThreadLocal的set方法在线程池创建线程之前,而不生效场景InheritableThreadLocal的set方法在创建线程之后(线程池内已经存在了可供使用的核心线程)。
对于父线程向已存在的线程传递数据需要用到阿里的TransmittableThreadLocal。
官网:https://github.com/alibaba/transmittable-thread-local
com.alibaba
transmittable-thread-local
2.14.5
/** * 使用1个线程的线程池 * 确保两次调用都是同一线程处理 */ private static final Executor ttl_executor = TtlExecutors.getTtlExecutor(Executors.newSingleThreadExecutor()); private static TransmittableThreadLocal threadLocal = new TransmittableThreadLocal<>();
public static void main(String args) throws InterruptedException {
ttl_executor.execute(()->{
System.out.println(“executorService创建线程”);
});
//线程休眠
Thread.sleep(100);
threadLocal.set(“传递变量”);
ttl_executor.execute(()->{
System.out.println(“获取变量 :”+threadLocal.get());;
});
}
executorService创建线程
获取变量 :传递变量
1. TransmittableThreadLocal.set
对于TransmittableThreadLocal的介绍还是从简单使用入手,TransmittableThreadLocal的set方法做了哪些逻辑。
public final void set(T value) {
if (!disableIgnoreNullValueSemantics && value == null) {
// may set null to remove value
remove();
} else {
super.set(value);
addThisToHolder();
}
}
没有太复杂的逻辑,除了调用父类的set方法。主要在addThisToHolder
private void addThisToHolder() {
if (!holder.get().containsKey(this)) {
holder.get().put((TransmittableThreadLocal) this, null); // WeakHashMap supports null value.
}
}
// Note about the holder:
// 1. holder self is a InheritableThreadLocal(a ThreadLocal).
// 2. The type of value in the holder is WeakHashMap<transmittablethreadlocal, ?>.
// 2.1 but the WeakHashMap is used as a Set:
// the value of WeakHashMap is always null, and never used.
// 2.2 WeakHashMap support null value.
private static final InheritableThreadLocal<weakhashmap<transmittablethreadlocal, ?>> holder =
new InheritableThreadLocal<weakhashmap<transmittablethreadlocal, ?>>() {
@Override
protected WeakHashMap<transmittablethreadlocal, ?> initialValue() {
return new WeakHashMap<>();
}
@Override
protected WeakHashMap<transmittablethreadlocal<object>, ?> childValue(WeakHashMap<transmittablethreadlocal<object>, ?> parentValue) {
return new WeakHashMap<>(parentValue);
}
};
第一个重要变量holder,根据官方的代码注释可以把它理解为一个set集合。里面的值是各个TransmittableThreadLocal对象,也就是说项目内通过TransmittableThreadLocal.set方法设置了值的TransmittableThreadLocal对象都在这个holder中。
2. TtlRunnable
根据官网的介绍TransmittableThreadLocal的实现原理。是使用TtlRunnable和TtlCallable来修饰传入线程池的Runnable和Callable。上面示例使用的TtlExecutors.getTtlExecutor最终也是执行Runnable和Callable.下面是官网提供的调用时序图。
public final class TtlRunnable implements Runnable, TtlWrapper, TtlEnhanced, TtlAttachments {
private final AtomicReference capturedRef;
private final Runnable runnable;
private final boolean releaseTtlValueReferenceAfterRun;
private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
//capture用于捕获父线程的ttl,会将当父线程上下文保存起来;
this.capturedRef = new AtomicReference<>(capture());
this.runnable = runnable;
this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}
/**
* wrap method {@link Runnable#run()}.
*/
@Override
public void run() {
//取出ttl
final Capture captured = capturedRef.get();
if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
throw new IllegalStateException("TTL value reference is released after run!");
}
//给当前子线程将父线程的TTL循环复制进子线程,
//返回的backup是此子线程原来就有的本地变量值(子线程的TTLMap);
//backup用于恢复数据(如果任务执行完毕,意味着该子线程会归还线程池,那么需要将其原生本地变量属性恢复)
final Backup backup = replay(captured);
try {
runnable.run();
} finally {
//方法用来恢复原有值的
restore(backup);
}
}
...
}
看到这里其实就可以捋清TransmittableThreadLocal的大致流程了。使用自定义的类TtlRunnable对Runnable进行扩展。线程池新建执行任务时(TtlRunnable)获取到项目中所有的ttl(使用holder做记录,还需要执行转换逻辑),执行run方法前取出TransmittableThreadLocal的快照(capture方法)并复制给将要执行任务的线程并返回backup(子线程原本的变量),在run方法执行结束后再将backup恢复给子线程。
3. Transmittee
经过上面的分析对于Transmittee的逻辑就方便理解了。主要就是TTL快照数据的获取与转换,根据代码和注释理解起来不成问题,就不展开讨论了。
private static class TtlTransmittee implements Transmittee<hashmap<transmittablethreadlocal, Object>, HashMap<transmittablethreadlocal, Object>> {
@NonNull
@Override
public HashMap<transmittablethreadlocal, Object> capture() {
final HashMap<transmittablethreadlocal, Object> ttl2Value = newHashMap(holder.get().size());
for (TransmittableThreadLocal threadLocal : holder.get().keySet()) {
ttl2Value.put(threadLocal, threadLocal.getTransmitteeValue());
}
return ttl2Value;
}
@NonNull
@Override
public HashMap<transmittablethreadlocal<object>, Object> replay(@NonNull HashMap<transmittablethreadlocal<object>, Object> captured) {
final HashMap<transmittablethreadlocal<object>, Object> backup = newHashMap(holder.get().size());
for (final Iterator<transmittablethreadlocal<object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
TransmittableThreadLocal<object> threadLocal = iterator.next();
// backup
backup.put(threadLocal, threadLocal.get());
// clear the TTL values that is not in captured
// avoid the extra TTL values after replay when run task
if (!captured.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
}
}
// set TTL values to captured
setTtlValuesTo(captured);
return backup;
}
@NonNull
@Override
public HashMap<transmittablethreadlocal<object>, Object> clear() {
return replay(newHashMap(0));
}
@Override
public void restore(@NonNull HashMap<transmittablethreadlocal<object>, Object> backup) {
for (final Iterator<transmittablethreadlocal<object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
TransmittableThreadLocal<object> threadLocal = iterator.next();
// clear the TTL values that is not in backup
// avoid the extra TTL values after restore
if (!backup.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
}
}
// restore TTL values
setTtlValuesTo(backup);
}
}
对于线程任务的封装除了修饰Runnable和Callable和修饰线程池(推荐)外。还提供了使用Java Agent来修饰JDK线程池实现类(和SkyWalking的思路一致)
4. TTL使用场景
- 分布式跟踪系统 或 全链路压测(即链路打标)
- 日志收集记录系统上下文
- Session级Cache
- 应用容器或上层框架跨应用代码给下层SDK传递信息
四. MDC线程间的数据传递
MDC(Mapped Diagnostic Context,映射调试上下文)是 slf4j 提供的一种轻量级的日志跟踪工具。
Log4j、Logback或者Log4j2等日志中最常见区分同一个请求的方式是通过线程名,而如果请求量大,线程名在相近的时间内会有很多重复的而无法分辨,因此引出了trace-id,即在接收到的时候生成唯一的请求id,在整个执行链路中带上此唯一id。
MDC.java本身不提供传递traceId的能力,真正提供能力的是MDCAdapter接口的实现。比如Log4j的是Log4jMDCAdapter,Logback的是LogbackMDCAdapter。
MDC使用的是ThreadLocal和InheritableThreadLocal,可以做到本线程间或者新建子线程的数据传递。对于实际使用中涉及线程池的数据传递需要一些改造。
1. MDC的包装类
新建MDCRunnable对Runnable进行封装
public class MDCRunnable implements Runnable{
private Map<string, string=""> mdcContext;
private Runnable runnable;
public MDCRunnable(Runnable runnable) {
this.runnable = runnable;
this.mdcContext = MDC.getCopyOfContextMap();
}
@Override
public void run() {
if (mdcContext != null) {
MDC.setContextMap(mdcContext);
}
try {
runnable.run();
} finally {
MDC.clear();
}
}
}
</string,>
在线程池执行任务时使用MDCRunnable来包装任务
/**
* 使用1个线程的线程池
* 确保两次调用都是同一线程处理
*/
private static ExecutorService executorService = Executors.newSingleThreadExecutor();
public static void main(String args) throws InterruptedException {
executorService.execute(new MDCRunnable(()->{
System.out.println(“executorService创建线程”);
}));
//线程休眠
Thread.sleep(100);
MDC.put(“traceId”,“9527”);
executorService.execute(new MDCRunnable(()->{
System.out.println(“获取变量 :”+MDC.get(“traceId”));
}));
}
executorService创建线程
获取变量 :9527
2. MDCAdapter的扩展
这部分的扩展是来自TransmittableThreadLocal对MDC的支持,目前对于Log4J2、Logback都实现了扩展方式。
3. 总结
从方法上来看实现对线程池内循环使用线程的数据传递。基本思路都是在InheritableThreadLocal的基础上进行扩展。在执行Runnable或Callable任务时对其包装,在真正调用线程run方法前后设置数据快照的复制替换和传递。比较简单的做法是实现包装类(比如上面提到的MDCRunnable)进行处理。但是这种方式是对代码入侵的,对开发来讲不太友好。比较好的方式是使用java agent来扩展,对字节码进行增强。例如TransmittableThreadLocal和SkyWalking都使用了这种方式,无需改动代码即可实现。
最后感谢大家观看!如有帮助 感谢支持
这是一个从 https://juejin.cn/post/7368678002235850791 下的原始话题分离的讨论话题
</transmittablethreadlocal</transmittablethreadlocal</transmittablethreadlocal</transmittablethreadlocal</transmittablethreadlocal</transmittablethreadlocal</transmittablethreadlocal</transmittablethreadlocal</transmittablethreadlocal</transmittablethreadlocal</hashmap<transmittablethreadlocal
</transmittablethreadlocal</transmittablethreadlocal</transmittablethreadlocal</weakhashmap<transmittablethreadlocal</weakhashmap<transmittablethreadlocal</transmittablethreadlocal