├── README.md
├── pom.xml
└── src
├── main
└── java
│ └── com
│ └── github
│ └── xjjdog
│ └── passthrough
│ ├── PassThroughFactory.java
│ ├── hystrix
│ ├── CommonHystrixConcurrencyStrategy.java
│ └── HystrixFactory.java
│ ├── mdc
│ ├── MDCCallable.java
│ └── MDCRunnable.java
│ └── threadlocal
│ ├── ThreadLocal.java
│ ├── ThreadLocalCallable.java
│ ├── ThreadLocalHandler.java
│ ├── ThreadLocalRunnable.java
│ └── Transmissible.java
└── test
└── java
└── com
└── github
└── xjjdog
└── passthrough
└── threadlocal
├── SimpleThreadLocalNotOkTest.java
├── SimpleThreadLocalOkTest.java
└── ThreadLocalTest.java
/README.md:
--------------------------------------------------------------------------------
1 | # 你的也是我的。3例ko多线程,局部变量透传
2 | >原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。
3 |
4 | java中的threadlocal,是绑定在线程上的。你在一个线程中set的值,在另外一个线程是拿不到的。如果在threadlocal的平行线程中,创建了新的子线程,那么这里面的值是无法传递、共享的(先想清楚为什么再往下看)。这就是透传问题。
5 |
6 | 值在线程之间的透传,你可以认为是一个bug,这些问题一般会比较隐蔽,但问题暴露的时候脾气却比较火爆,让人手忙脚乱,怀疑人生。
7 |
8 | 作为代码的掌舵者,我们必然不能忍受这种问题的蹂躏。本篇文章适合细看,我们拿出3个例子,通过编码手段说明解决此类bug的通用方式,希望能达到举一反三的效果。对于搞基础架构的同学,是必备知识点。
9 |
10 | 1、普通线程的ThreadLocal透传问题
11 | 2、sl4j MDC组件中ThreadLocal透传问题
12 | 3、Hystrix组件的透传问题
13 |
14 | 由于涉及代码比较多,xjjdog将这三个例子的代码,放在了github上,想深入研究,可以下载下来debug一下。
15 | ```
16 | https://github.com/xjjdog/example-pass-through
17 | ```
18 |
19 | # 一、问题简单演示
20 |
21 | 为了有个比较直观的认识,下面展示一段异常代码。
22 | 
23 | 以上代码在主线程设置了一个简单的threadlocal变量,然后在自线程中想要取出它的值。执行后发现,程序的输出是:`null`。
24 |
25 | 程序的输出和我们的期望产生了明显的差异。其实,将**ThreadLocal** 换成**InheritableThreadLocal** 就ok了。不要高兴太早,对于使用线程池的情况,由于会缓存线程,线程是缓存起来反复使用的。这时父子线程关系的上下文传递,已经没有意义。
26 |
27 | # 二、解决线程池透传问题
28 |
29 | 所以,线程池InheritableThreadLocal进行提交,获取的值,有可能是前一个任务执行后留下的,是错误的。使用只有在任务执行的时候进行传递,才是正常的功能。
30 |
31 | 上面的问题,transmittable-thread-local项目,已经很好的解决,并提供了java-agent的方式支持。
32 |
33 | 我们这里从最小集合的源码层面,来看一下其中的内容。首先,我们看一下ThreadLocal的结构。
34 | 
35 | ThreadLocal其实是作为一个Map中的key而存在的,这个Map就是ThreadLocalMap,它以私有变量的形式,存在于Thread类中。拿上图为例,如果我创建了一个ThreadLocal,然后调用set方法,它会首先找到当前的thread,然后找到threadLocals,最后把自己作为key,存放在这个map里。
36 | ```
37 | hread t = Thread.currentThread();
38 | ThreadLocalMap map = getMap(t);
39 | map.set(this, value);
40 | ```
41 |
42 | 要能够完成多线程的协调工作,必须提供全套的多线程工具。包括但不限于:
43 |
44 | **1、定义注解,以及被注解修饰的ThreadLocal类**
45 | 
46 | 定义新的ThreadLocal类,以便在赋值的时候,能够根据注解进行拦截和过滤。这就要求,在定义ThreadLocal的时候,要使用我们提供的ThreadLocal类,而不是jdk提供的那两个。
47 |
48 | **2、进行父子线程之间的数据拷贝**
49 | 在线程池提交任务之前,我们需要有个地方,将父进程的ThreadLocal内容,暂存一下。
50 | 
51 | 由于很多变量都是private的,需要根据反射进行操作。根据上面提供的ThreadLocal类的结构,我们需要直接操作其中的变量table(这也是为什么jdk不能随便改变变量名的原因)。
52 |
53 | 将父线程相关的变量暂存之后,就可以在使用的时候,通过主动设值和清理,完成变量拷贝。
54 |
55 | **3、提供专用的Callable或者Runnable**
56 | 那么这些数据是如何组装起来的呢?还是靠我们的任务载体类。
57 | 线程池提交线程,一般是通过Callable或者Runnable,以Runnable为例,我们看一下这个调用关系。
58 |
59 | 以下类采用了委托模式。
60 | 
61 |
62 | 这样,只要在提交任务的时候,使用了我们自定义的Runnable;同时,使用了自定义的ThreadLocal,就能够正常完成透传。
63 |
64 | # 三、解决MDC透传问题
65 |
66 | sl4j MDC机制非常好,通常用于保存线程本地的“诊断数据”然后有日志组件打印,其内部时基于threadLocal实现;不过这就有一些问题,主线程中设置的MDC数据,在其子线程(多线程池)中是无法获取的,下面就来介绍如何解决这个问题。
67 |
68 | >MDC ( Mapped Diagnostic Contexts ),它是一个线程安全的存放诊断日志的容器。通常,会在处理请求前将请求的唯一标示放到MDC容器中,比如sessionId。这个唯一标示会随着日志一起输出。配置文件可以使用占位符进行变量替换。
69 |
70 | 类似于上面介绍的方式,我们需要提供专用的Callable和Runnable。另外,为了能够同时支持MDC和普通线程,这两个类采用装饰器模式,进行功能追加。就单个类来说,对外的展现依然是委托模式。
71 | 
72 | 同样的思路,同样的模式。不一样的是,父线程的信息暂存,我们直接使用MDC的内部方法,并在任务的执行前后,进行相应操作。
73 |
74 | # 四、解决Hystrix透传问题
75 |
76 | 同样的问题,在Netflix公司的熔断组件Hystrix中,依然存在。Hystrix线程池模式下,透传ThreadLocal需要进行改造,它本身是无法完成这个功能的。
77 |
78 | 但是Hystrix策略无法简单通过yml文件方式配置。我们参考Spring Cloud中对此策略的扩展方式,开发自己的策略。需要继承HystrixConcurrentStrategy。
79 |
80 | 构造代码还是较长的,可以查看github项目。但有一个地方需要说明。
81 | 
82 | 我们使用装饰器模式,对代码进行了层层嵌套,同时将多线程透传功能、MDC传递功能给追加了进来。这样,我们的这个类,就同时在以上三个环境中拥有了透传功能。
83 |
84 | # End
85 | 同样的思路,可以用在其他组件上。比如我们在多篇调用链的文章里,提到的trace信息在多线程环境下的传递。
86 |
87 | 一般就是在当前线程暂存数据,然后在提交任务时进行包装。值得注意的是,这种方式侵入性还是比较大的,适合封装在通用的基础工具包中。你要是在业务中这么用,大概率会被骂死。
88 |
89 | 那可如何是好。
90 |
91 | ThreadLocal会引发很多棘手的bug,造成代码污染。在使用之前,一定要确保你确实需要使用它。比如你在SimpleDateFormat类上用了线程局部变量,可以将它替换成DateTimeFormatter。
92 |
93 | 我们不善于解决问题,我们只善于解决容易出问题的类。
94 |
95 | >作者简介:**小姐姐味道** (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流。
96 |
97 | 近期热门文章
98 |
99 | [《必看!java后端,亮剑诛仙》](https://mp.weixin.qq.com/s/Cuv0SyjzasDKC0wIQxrgaw)
100 | 后端技术索引,中肯火爆
101 |
102 | [《Linux上,最常用的一批命令解析(10年精选)》](https://mp.weixin.qq.com/s/9RbTGQ4k4s92mrSf2xJ5TQ)
103 | CSDN发布首日,1k赞。点赞率1/8。
104 |
105 | [《这次要是讲不明白Spring Cloud核心组件,那我就白编这故事了》](https://mp.weixin.qq.com/s/hjYAddJEqgg3ZWTJnPTD9g)
106 | 用故事讲解核心组件,包你满意
107 |
108 | [《Linux生产环境上,最常用的一套“Sed“技巧》](https://mp.weixin.qq.com/s/wP9_wvoTARRrlszsOmvMgQ)
109 | 最常用系列Sed篇,简单易懂。Vim篇更加易懂。
110 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | com.github.xjjdog
8 | example-pass-through
9 | 1.0.0
10 |
11 |
12 | ch.qos.logback
13 | logback-access
14 | 1.2.3
15 |
16 |
17 | ch.qos.logback
18 | logback-classic
19 | 1.2.3
20 |
21 |
22 |
23 | ch.qos.logback
24 | logback-core
25 | 1.2.3
26 |
27 |
28 |
29 | com.netflix.hystrix
30 | hystrix-core
31 | 1.5.12
32 |
33 |
34 |
35 | org.junit.jupiter
36 | junit-jupiter-api
37 | RELEASE
38 | test
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/main/java/com/github/xjjdog/passthrough/PassThroughFactory.java:
--------------------------------------------------------------------------------
1 | package com.github.xjjdog.passthrough;
2 |
3 | public class PassThroughFactory {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/com/github/xjjdog/passthrough/hystrix/CommonHystrixConcurrencyStrategy.java:
--------------------------------------------------------------------------------
1 | package com.github.xjjdog.passthrough.hystrix;
2 |
3 | import com.github.xjjdog.passthrough.mdc.MDCCallable;
4 | import com.github.xjjdog.passthrough.threadlocal.ThreadLocalCallable;
5 | import com.netflix.hystrix.HystrixThreadPoolKey;
6 | import com.netflix.hystrix.HystrixThreadPoolProperties;
7 | import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;
8 | import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariable;
9 | import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableLifecycle;
10 | import com.netflix.hystrix.strategy.properties.HystrixProperty;
11 |
12 | import java.util.concurrent.BlockingQueue;
13 | import java.util.concurrent.Callable;
14 | import java.util.concurrent.ThreadPoolExecutor;
15 | import java.util.concurrent.TimeUnit;
16 |
17 | public class CommonHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
18 |
19 | private HystrixConcurrencyStrategy delegate;
20 |
21 | public CommonHystrixConcurrencyStrategy(HystrixConcurrencyStrategy delegate) {
22 | this.delegate = delegate;
23 | }
24 |
25 | @Override
26 | public Callable wrapCallable(Callable callable) {
27 | Callable wrapped = this.delegate != null
28 | ? this.delegate.wrapCallable(callable) : callable;
29 | if (wrapped instanceof ThreadLocalCallable) {
30 | return wrapped;
31 | }
32 | if (wrapped instanceof MDCCallable) {
33 | return new ThreadLocalCallable<>(wrapped);
34 | }
35 | return new ThreadLocalCallable<>(new MDCCallable<>(wrapped));
36 | }
37 |
38 |
39 | @Override
40 | public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
41 | HystrixProperty corePoolSize,
42 | HystrixProperty maximumPoolSize,
43 | HystrixProperty keepAliveTime, TimeUnit unit,
44 | BlockingQueue workQueue) {
45 | return this.delegate.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize,
46 | keepAliveTime, unit, workQueue);
47 | }
48 |
49 | @Override
50 | public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
51 | HystrixThreadPoolProperties threadPoolProperties) {
52 | return this.delegate.getThreadPool(threadPoolKey, threadPoolProperties);
53 | }
54 |
55 | @Override
56 | public BlockingQueue getBlockingQueue(int maxQueueSize) {
57 | return this.delegate.getBlockingQueue(maxQueueSize);
58 | }
59 |
60 | @Override
61 | public HystrixRequestVariable getRequestVariable(
62 | HystrixRequestVariableLifecycle rv) {
63 | return this.delegate.getRequestVariable(rv);
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/java/com/github/xjjdog/passthrough/hystrix/HystrixFactory.java:
--------------------------------------------------------------------------------
1 | package com.github.xjjdog.passthrough.hystrix;
2 |
3 | import com.netflix.hystrix.strategy.HystrixPlugins;
4 | import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;
5 | import com.netflix.hystrix.strategy.eventnotifier.HystrixEventNotifier;
6 | import com.netflix.hystrix.strategy.executionhook.HystrixCommandExecutionHook;
7 | import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher;
8 | import com.netflix.hystrix.strategy.properties.HystrixPropertiesStrategy;
9 |
10 | public class HystrixFactory {
11 | public void init() {
12 | HystrixConcurrencyStrategy delegate = HystrixPlugins.getInstance().getConcurrencyStrategy();
13 | if (delegate instanceof CommonHystrixConcurrencyStrategy) {
14 | return;
15 | }
16 | HystrixConcurrencyStrategy strategy = new CommonHystrixConcurrencyStrategy(delegate);
17 |
18 | HystrixCommandExecutionHook commandExecutionHook = HystrixPlugins
19 | .getInstance().getCommandExecutionHook();
20 | HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance()
21 | .getEventNotifier();
22 | HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance()
23 | .getMetricsPublisher();
24 | HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins.getInstance()
25 | .getPropertiesStrategy();
26 |
27 | HystrixPlugins.reset();//set all null
28 |
29 | HystrixPlugins.getInstance().registerConcurrencyStrategy(strategy);
30 | HystrixPlugins.getInstance()
31 | .registerCommandExecutionHook(commandExecutionHook);
32 | HystrixPlugins.getInstance().registerEventNotifier(eventNotifier);
33 | HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher);
34 | HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/com/github/xjjdog/passthrough/mdc/MDCCallable.java:
--------------------------------------------------------------------------------
1 | package com.github.xjjdog.passthrough.mdc;
2 |
3 |
4 | import org.slf4j.MDC;
5 |
6 | import java.util.Map;
7 | import java.util.concurrent.Callable;
8 |
9 | public class MDCCallable implements Callable {
10 |
11 | private final Callable callable;
12 |
13 | private transient final Map _cm = MDC.getCopyOfContextMap();
14 |
15 | public MDCCallable(Callable callable) {
16 | this.callable = callable;
17 | }
18 |
19 | @Override
20 | public V call() throws Exception {
21 | if (_cm != null) {
22 | MDC.setContextMap(_cm);
23 | }
24 | try {
25 | return callable.call();
26 | } finally {
27 | MDC.clear();
28 | }
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/com/github/xjjdog/passthrough/mdc/MDCRunnable.java:
--------------------------------------------------------------------------------
1 | package com.github.xjjdog.passthrough.mdc;
2 |
3 | import org.slf4j.MDC;
4 |
5 | import java.util.Map;
6 |
7 | public class MDCRunnable implements Runnable {
8 |
9 | private final Runnable runnable;
10 |
11 | private transient final Map _cm = MDC.getCopyOfContextMap();
12 |
13 | public MDCRunnable(Runnable runnable) {
14 | this.runnable = runnable;
15 | }
16 |
17 | @Override
18 | public void run() {
19 | if (_cm != null) {
20 | MDC.setContextMap(_cm);
21 | }
22 | try {
23 | runnable.run();
24 | } finally {
25 | MDC.clear();
26 | }
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/com/github/xjjdog/passthrough/threadlocal/ThreadLocal.java:
--------------------------------------------------------------------------------
1 | package com.github.xjjdog.passthrough.threadlocal;
2 |
3 | @Transmissible
4 | public class ThreadLocal extends java.lang.ThreadLocal {
5 |
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/java/com/github/xjjdog/passthrough/threadlocal/ThreadLocalCallable.java:
--------------------------------------------------------------------------------
1 | package com.github.xjjdog.passthrough.threadlocal;
2 |
3 | import java.util.concurrent.Callable;
4 |
5 | public class ThreadLocalCallable implements Callable {
6 |
7 | private final Callable callable;
8 |
9 | private transient ThreadLocalHandler.Context _cm = ThreadLocalHandler.handle();
10 |
11 | public ThreadLocalCallable(Callable callable) {
12 | this.callable = callable;
13 | }
14 |
15 | @Override
16 | public V call() throws Exception {
17 | if (_cm != null) {
18 | _cm.set();
19 | }
20 | try {
21 | return this.callable.call();
22 | } finally {
23 | if (_cm != null) {
24 | _cm.remove();
25 | }
26 | }
27 | }
28 |
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/com/github/xjjdog/passthrough/threadlocal/ThreadLocalHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.xjjdog.passthrough.threadlocal;
2 |
3 | import java.lang.ThreadLocal;
4 | import java.lang.reflect.Array;
5 | import java.lang.reflect.Field;
6 | import java.lang.reflect.InvocationTargetException;
7 | import java.lang.reflect.Method;
8 | import java.util.Map;
9 | import java.util.WeakHashMap;
10 |
11 | public class ThreadLocalHandler {
12 |
13 | private static Field THREAD_LOCALS;
14 |
15 | private static Field THREAD_LOCAL_MAP_TABLE;
16 |
17 | private static Method THREAD_LOCAL_MAP_ENTRY_GET_METHOD;
18 |
19 | private static Field THREAD_LOCAL_MAP_ENTRY_VALUE;
20 |
21 | private static volatile boolean PREPARED = false;
22 |
23 | static {
24 | try {
25 | //ThreadLocal is java native-lib,loaded by bootstrap.
26 | //they(fields) can be cached safely.
27 | THREAD_LOCALS = Thread.class.getDeclaredField("threadLocals");
28 | THREAD_LOCALS.setAccessible(true);
29 |
30 | Class mapClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
31 | THREAD_LOCAL_MAP_TABLE = mapClass.getDeclaredField("table");
32 | THREAD_LOCAL_MAP_TABLE.setAccessible(true);
33 | Class entryClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap$Entry");
34 | THREAD_LOCAL_MAP_ENTRY_VALUE = entryClass.getDeclaredField("value");
35 | THREAD_LOCAL_MAP_ENTRY_VALUE.setAccessible(true);
36 |
37 | THREAD_LOCAL_MAP_ENTRY_GET_METHOD = entryClass.getMethod("get");
38 | THREAD_LOCAL_MAP_ENTRY_GET_METHOD.setAccessible(true);
39 |
40 | PREPARED = true;
41 | } catch (NoSuchFieldException | NoSuchMethodException | SecurityException | ClassNotFoundException e) {
42 | //
43 | }
44 | }
45 |
46 |
47 | /**
48 | * @return null if no threadLocals
49 | * @throws Exception
50 | */
51 | public static Context handle() {
52 | if (!PREPARED) {
53 | return null;
54 | }
55 |
56 | Thread thread = Thread.currentThread();
57 | try {
58 | //also private,reflection
59 | Object table = THREAD_LOCAL_MAP_TABLE.get(THREAD_LOCALS.get(thread));
60 | if (table == null) {
61 | return null;
62 | }
63 |
64 | int count = Array.getLength(table);
65 | if (count == 0) {
66 | return null;
67 | }
68 |
69 | //
70 | Map variables = new WeakHashMap<>();
71 | for (int i = 0; i < count; i++) {
72 | Object entry = Array.get(table, i);
73 | if (entry != null) {
74 | ThreadLocal key = (ThreadLocal) THREAD_LOCAL_MAP_ENTRY_GET_METHOD.invoke(entry);
75 | if (transmissible(key)) {
76 | Object value = THREAD_LOCAL_MAP_ENTRY_VALUE.get(entry);
77 | variables.put(key, value);
78 | }
79 | }
80 | }
81 | return new Context(variables);
82 | } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {
83 | //
84 | }
85 | return null;
86 | }
87 |
88 |
89 | private static boolean transmissible(ThreadLocal target) {
90 | return target == null ? false : target.getClass().isAnnotationPresent(Transmissible.class);
91 | }
92 |
93 |
94 | public static class Context {
95 | final Map variables;
96 | final Thread parent;
97 |
98 | protected Context(Map variables) {
99 | this.variables = variables;
100 | parent = Thread.currentThread();
101 | }
102 |
103 | public void set() {
104 | if (variables == null || variables.isEmpty() || parent == Thread.currentThread()) {
105 | return;
106 | }
107 | variables.forEach((threadLocal, value) -> {
108 | if (threadLocal != null) {
109 | threadLocal.set(value);
110 | }
111 | });
112 | }
113 |
114 | public void remove() {
115 | if (variables == null || variables.isEmpty()) {
116 | return;
117 | }
118 | variables.forEach((threadLocal, value) -> {
119 | if (threadLocal != null) {
120 | threadLocal.remove();
121 | }
122 | });
123 | variables.clear();
124 | }
125 | }
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/src/main/java/com/github/xjjdog/passthrough/threadlocal/ThreadLocalRunnable.java:
--------------------------------------------------------------------------------
1 | package com.github.xjjdog.passthrough.threadlocal;
2 |
3 | public class ThreadLocalRunnable implements Runnable {
4 |
5 | private final Runnable runnable;
6 |
7 | private transient final ThreadLocalHandler.Context _cm = ThreadLocalHandler.handle();
8 |
9 | public ThreadLocalRunnable(Runnable runnable) {
10 | this.runnable = runnable;
11 | }
12 |
13 | @Override
14 | public void run() {
15 | if (_cm != null) {
16 | _cm.set();
17 | }
18 | try {
19 | runnable.run();
20 | } finally {
21 | if (_cm != null) {
22 | _cm.remove();
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/github/xjjdog/passthrough/threadlocal/Transmissible.java:
--------------------------------------------------------------------------------
1 | package com.github.xjjdog.passthrough.threadlocal;
2 |
3 | import java.lang.annotation.*;
4 |
5 | @Target({ElementType.TYPE})
6 | @Retention(RetentionPolicy.RUNTIME)
7 | @Documented
8 | public @interface Transmissible {
9 | }
10 |
--------------------------------------------------------------------------------
/src/test/java/com/github/xjjdog/passthrough/threadlocal/SimpleThreadLocalNotOkTest.java:
--------------------------------------------------------------------------------
1 | package com.github.xjjdog.passthrough.threadlocal;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | public class SimpleThreadLocalNotOkTest {
6 | @Test
7 | void testThreadLocal(){
8 | ThreadLocal