logo头像
Snippet 博客主题

线程切换导致ThreadLocal数据丢失分析

本文于873天之前发表,文中内容可能已经过时

最近在使用Spring Cloud过程中,经常会遇见线程隔离(切换).导致ThreadLocal数据丢失.例如调用其他服务获取不到Threadlocal没有数据,服务之间传递请求头传递失败.通过查阅相关文档才发现:
用Hystrix实现断路器,Zuul中默认使用的是信号量,其他默认都是线程隔离.具体文档如下(可参考Hystrix WIKI):

Thread or Semaphore

  • The default, and the recommended setting, is to run HystrixCommands using thread isolation (THREAD) and HystrixObservableCommands using semaphore isolation (SEMAPHORE).
  • Commands executed in threads have an extra layer of protection against latencies beyond what network timeouts can offer.
  • Generally the only time you should use semaphore isolation for HystrixCommands is when the call is so high volume (hundreds per second, per instance) that the overhead of separate threads is too high; this typically only applies to non-network calls.

在使用线程隔离的时候,有个问题是必须要解决的,那就是在某些业务场景下通过ThreadLocal来在线程里传递数据,用信号量是没问题的,从请求进来,但后续的流程都是通一个线程。
当隔离模式为线程时,Hystrix会将请求放入Hystrix的线程池中去执行,这个时候某个请求就有A线程变成B线程了,ThreadLocal必然消失了.

模拟实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class CustomThreadLocal {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();

public static void main(String[] args) {
new Thread(new Runnable() {

@Override
public void run() {
CustomThreadLocal.threadLocal.set("彤哥哥");
new Service().call();

}
}).start();

}
}


class Service {
public void call() {
System.out.println("Service:" + Thread.currentThread().getName());
System.out.println("Service:" + CustomThreadLocal.threadLocal.get());
new Dao().call();
}
}

class Dao {
public void call() {
System.out.println("==========================");
System.out.println("Dao:" + Thread.currentThread().getName());
System.out.println("Dao:" + CustomThreadLocal.threadLocal.get());
}
}

我们在主类中定义了一个ThreadLocal用来传递数据,然后起了一个线程,在线程中调用Service中的call方法,并且往Threadlocal中设置了一个值,在Service中获取ThreadLocal中的值,然后再调用Dao中的call方法,也是获取ThreadLocal中的值,我们运行下看效果:

1
2
3
4
5
Service:Thread-0
Service:彤哥哥
==========================
Dao:Thread-0
Dao:彤哥哥

从运行结果来看,同一个线程中能够获得ThreadLocal的值.这个没错,接下来,将Serice类中的call()方法稍微改造一下:

1
2
3
4
5
6
7
8
9
10
11
public void call() {
System.out.println("Service:" + Thread.currentThread().getName());
System.out.println("Service:" + CustomThreadLocal.threadLocal.get());
//new Dao().call();
new Thread(new Runnable() {
@Override
public void run() {
new Dao().call();
}
}).start();
}

再次运行结果如下:

1
2
3
4
5
Service:Thread-0
Service:彤哥哥
==========================
Dao:Thread-1
Dao:null

由此可见是两个不同的线程,在运行Dao中的call()方法进行了线程切换,所以ThreadLocal获取到的数据未null.

InheritableThreadLocal引入

既然遇到问题就该解决,那么如何解决呢?
其实解决起来很简单,只需要改一行代码即可.

1
static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

将Threadlocal改成子类InheritableThreadLocal后运行结果:

1
2
3
4
5
Service:Thread-0
Service:彤哥哥
==========================
Dao:Thread-1
Dao:彤哥哥

非常完美的解决了线程切换导致ThreadLocal拿不到值而产生的问题.

深入InheritableThreadLocal原理

要先了解InheritableThreadLocal原理,首先清楚ThreadLocal的原理.话不多说,先分析一下ThreadLocal的原理:

  • 每个线程都有一个ThreadLocalMap类型的threadLocals属性,ThreadLocalMap类相当于一个Map,key 是 ThreadLocal本身,value就是我们设置的值.

    1
    2
    3
    public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
    }
  • 当我们通过threadLocal.set(“彤哥哥”)的时候,就是在这个线程中的threadLocals属性中放入一个键值对,key是 当前线程,value就是你设置的值。

  • 当我们通过 threadlocal.get()方法的时候,就是根据当前线程作为key来获取这个线程设置的值.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public class ThreadLocal<T> {

    public void set(T value) {
    //获取当前的线程对象
    Thread t = Thread.currentThread();
    //获取当前线程对象中的threadLocals属性
    ThreadLocalMap map = getMap(t);
    if (map != null)
    map.set(this, value);
    else
    createMap(t, value);
    }

    ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
    }

    public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
    @SuppressWarnings("unchecked")
    T result = (T)e.value;
    return result;
    }
    }
    return setInitialValue();
    }
    }

通过上面的介绍我们可以了解到threadlocal能够传递数据是用Thread.currentThread()当前线程来获取,也就是只要在相同的线程中就可以获取到前面设置进去的值.
如果在threadlocal设置完值之后,下步的操作重新创建了一个线程,这个时候Thread.currentThread()就已经变了,那么肯定是拿不到之前设置的值.具体的问题复现可以参考上面我的代码.
那为什么InheritableThreadLocal就可以呢?
InheritableThreadLocal这个类继承了ThreadLocal,重写了3个方法,在当前线程上创建一个新的线程实例Thread时,会把这些线程变量从当前线程传递给新的线程实例.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
}
}

通过上面的代码我们可以看到InheritableThreadLocal 重写了childValue, getMap,createMap三个方法,我们往里面set值的时候,值保存到了inheritableThreadLocals里面,而不是之前的threadLocals
那么关键的点来了,为什么当创建新的线程池,可以获取到上个线程里的threadLocal中的值呢?原因就是在新创建线程的时候,会把之前线程的inheritableThreadLocals赋值给新线程的inheritableThreadLocals,通过这种方式实现了数据的传递.
源码最开始在Thread的init()方法中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public
class Thread implements Runnable {
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//代码省略......
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
//创建新的ThreadLocalMap并复制给当前线程的inheritableThreadLocals对象
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//代码省略......
}

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}

/**
* 赋值代码
*/
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];

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) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
}

到此为止,通过inheritableThreadLocals我们可以在父线程创建子线程的时候将Local中的值传递给子线程,这个特性已经能够满足大部分的需求了.
但是还有一个很严重的问题是如果是在线程复用的情况下就会出问题,比如线程池中去使用inheritableThreadLocals进行传值,因为inheritableThreadLocals 只是会再新创建线程的时候进行传值,
线程复用并不会做这个操作,那么要解决这个问题就得自己去扩展线程类实现这个功能.

阿里解决之道

开源的世界应有尽有,为了解决上述遗留的问题,阿里开源了一款Java框架: transmittable-thread-local
其主要功能就是解决在使用线程池等会缓存线程的组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题.
JDK的InheritableThreadLocal类可以完成父线程到子线程的值传递.但对于使用线程池等会缓存线程的组件的情况,线程由线程池创建好,并且线程是缓存起来反复使用的;
这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把任务提交给线程池时的ThreadLocal值传递到任务执行时.
transmittable-thread-local使用方式分为三种:修饰Runnable和Callable,修饰线程池,Java Agent来修饰JDK线程池实现类.
接下来给大家演示下线程池的修饰方式,首先来一个非正常的案例,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class CustomThreadLocal {

static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
/**
* 创建一个固定大小为2的线程池
*/
static ExecutorService pool = Executors.newFixedThreadPool(2);

public static void main(String[] args) {
for(int i=0;i<100;i++) {
int j = i;
pool.execute(new Thread(new Runnable() {

@Override
public void run() {
CustomThreadLocal.threadLocal.set("彤哥哥"+j);
new Service().call();
}
}));
}

}
}
class Service {
public void call() {
CustomThreadLocal.pool.execute(new Runnable() {

@Override
public void run() {
new Dao().call();
}
});

}
}

class Dao {
public void call() {
System.out.println("Dao:" + CustomThreadLocal.threadLocal.get());
}
}

运行上面的代码,出现的结果是不正确的,输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Dao:彤哥哥99
Dao:彤哥哥99
Dao:彤哥哥62
Dao:彤哥哥99
Dao:彤哥哥62
Dao:彤哥哥99
Dao:彤哥哥99
Dao:彤哥哥62
Dao:彤哥哥99
Dao:彤哥哥62
Dao:彤哥哥62
Dao:彤哥哥99
省略之后的结果...

正确的应该是从0-99不能有重复,由于线程的复用,值被替换掉了才会出现不正确的结果.
接下来使用transmittable-thread-local来改造有问题的代码,添加transmittable-thread-local的Maven依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.2.0</version>
</dependency>

只需要修改2个地方,修饰线程池和替换InheritableThreadLocal:

1
2
static TransmittableThreadLocal<String> threadLocal = new TransmittableThreadLocal<>();
static ExecutorService pool = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(2));

正确结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
Dao:彤哥哥1
Dao:彤哥哥2
Dao:彤哥哥4
Dao:彤哥哥5
Dao:彤哥哥3
Dao:彤哥哥6
Dao:彤哥哥7
Dao:彤哥哥8
Dao:彤哥哥9
Dao:彤哥哥10
Dao:彤哥哥11
省略之后的结果...

到这里我们就已经可以完美的解决线程中,线程池中ThreadLocal数据的传递了,题主趁着中午喝茶的时间,无意间找到其他解决方案,下节继续用其他方式进行解决.

支付宝打赏 微信打赏

请作者喝杯咖啡吧