概述
SpringCloud中Hystrix选择线程池进行隔离时导致的ThreadLocal数据丢失的解决方法参考
最近在复习ThreadLocal时,新学到了一些有意思的好知识。
当我们在SpringCloud中选择Hystrix来实现断路器,Zuul中默认是用信号量,而Hystrix默认是线程池来进行隔离的。
当使用线程隔离时,会有一个很重要的问题需要注意:
那就是在一些业务场景下,可能需要ThreadLocal里在线程里传递数据,当然,如果你使用信号量的话是没问题的(信号量,请求进来的时候,以及后续的处理都是通过一个线程,这个是没问题的)。
当隔离模式选择了线程池时,Hystrix会将请求放到Hystrix的线程池里去执行,这个时候就会出现一个现象:当前请求线程 A 经过Hytrix包装和,会变成B线程去请求远程服务,这个时候A线程的ThreadLocal中的数据就不能在B线程中获取了。
简单模拟一下
1. 同一个线程中的情况
public class SpringCloudHystrixThreadLocal {
public static ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(()->{
stringThreadLocal.set("SaltIce Data");
new LocalService().call();
}).start();
}
}
class LocalService{
public void call(){
System.out.println("LocalService:"+ Thread.currentThread().getName());
System.out.println("LocalService:"+ SpringCloudHystrixThreadLocal.stringThreadLocal.get());
System.out.println("=======================================");
new RemoteService().call();
}
}
class RemoteService{
public void call(){
System.out.println("RemoteService:"+ Thread.currentThread().getName());
System.out.println("RemoteService:"+ SpringCloudHystrixThreadLocal.stringThreadLocal.get());
}
}
在主类中定义了一个ThreadLocal来传递数据,然后主类中模拟了一个请求线程,在线程中设置了一个值为SaltIce Data,并且调用了LocalService的call()方法,在LocalService.call()方法中获取了这个值,并且调用了RemoteService.call()方法,并且在里面也去获取了这个值,从下面结果可以看到,这个是在一个线程中完成全部的,这个是可以获取到ThreadLocal的值的。
LocalService:Thread-0
LocalService:SaltIce Data
=======================================
RemoteService:Thread-0
RemoteService:SaltIce Data
2. 改变一下,不同线程中的情况
改变一下LocalService.call()中调用RemoteService.call()的方式,通过另外起一个线程去调用,这个就与Hystrix的方式有点类似了。
public class SpringCloudHystrixThreadLocal {
public static ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(()->{
stringThreadLocal.set("SaltIce Data");
new LocalService().call();
}).start();
}
}
class LocalService{
public void call(){
System.out.println("LocalService:"+ Thread.currentThread().getName());
System.out.println("LocalService:"+ SpringCloudHystrixThreadLocal.stringThreadLocal.get());
System.out.println("=======================================");
//new RemoteService().call();
new Thread(()->{
new RemoteService().call();
}).start();
}
}
class RemoteService{
public void call(){
System.out.println("RemoteService:"+ Thread.currentThread().getName());
System.out.println("RemoteService:"+ SpringCloudHystrixThreadLocal.stringThreadLocal.get());
}
}
运行效果:
LocalService:Thread-0
LocalService:SaltIce Data
=======================================
RemoteService:Thread-1
RemoteService:null
可以看到,这两个都是在不同的线程中运行,而RemoteService.call()方法中却获取不到值,这个是与ThreadLocal的设计有关。那有什么办法吗?
该模拟的解决方法
改一行代码就可以解决:
static ThreadLocal<String> stringThreadLocal = new InheritableThreadLocal<>();
就只需将ThreadLocal改为InheritableThreadLocal即可:
我们看一下修改后的效果:
LocalService:Thread-0
LocalService:SaltIce Data
=======================================
RemoteService:Thread-1
RemoteService:SaltIce Data
可以看到,在不同的线程,也可以拿到值。
InheritableThreadLocal就是专门为了解决这种线程切换导致的ThreadLocal拿不到值的问题。这个类就不详谈了,就简单介绍一下核心代码。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* Computes the child's initial value for this inheritable thread-local
* variable as a function of the parent's value at the time the child
* thread is created. This method is called from within the parent
* thread before the child is started.
* <p>
* This method merely returns its input argument, and should be overridden
* if a different behavior is desired.
*
* @param parentValue the parent thread's value
* @return the child thread's initial value
*/
protected T childValue(T parentValue) {
return parentValue;
}
/**
* Get the map associated with a ThreadLocal.
*
* @param t the current thread
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* Create the map associated with a ThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the table.
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
它继承了ThreadLocal,并重写了里面三个方法,当我们往里面set值的时候,值保存在了线程的inheritableThreadLocals里面,而不是之前的threadLocals
Thread类里面有两个属性:
/* 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的时候,是使用了threadLocals。
关键点,为什么当创建新的线程时,可以获取到上个线程的threadLocals中的值呢?
原因在于,创建新线程的时候,会把之前线程的inheritableThreadLocals赋值给新线程的inheritableThreadLocals,这样就实现了数据的传递。
在Thread的init方法中有相应的源码
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
createInheritedMap源码如下:
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 只是会在创建新线程的时候进行传值,线程复用并不会有这个操作。要解决这个问题就需要自己去扩展线程类,实现这个功能了。
但是,阿里开源了一个好东西:transmittable-thread-local。
主要功能是 解决在使用线程池等会缓存线程的组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。
这个java库在github上面有详细的文档介绍。
Github地址:https://github.com/alibaba/transmittable-thread-local
最优解决方案
对比看一下:
1. 只使用InheritableThreadLocal和线程池复用的场景
public class SpringCloudHystrixThreadLocalPlus {
static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
static ExecutorService executors = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
int j = i;
executors.submit(()->{
threadLocal.set("SaltIce-"+j);
new LocalService2().call();
});
}
try {
Thread.sleep(1000);
executors.shutdown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class LocalService2{
public void call(){
SpringCloudHystrixThreadLocalPlus.executors.submit(()->{
new RemoteService2().call();
});
}
}
class RemoteService2{
public void call(){
System.out.println("RemoteService2:"+ SpringCloudHystrixThreadLocalPlus.threadLocal.get());
}
}
运行结果,我们期望的0-9都出现一次
RemoteService2:SaltIce-8
RemoteService2:SaltIce-9
RemoteService2:SaltIce-8
RemoteService2:SaltIce-9
RemoteService2:SaltIce-8
RemoteService2:SaltIce-9
RemoteService2:SaltIce-8
RemoteService2:SaltIce-9
RemoteService2:SaltIce-8
RemoteService2:SaltIce-9
2. 使用阿里的Java库:transmittable-thread-local
首先引入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.11.5</version>
</dependency>
修改两行代码即可
static ThreadLocal<String> threadLocal = new TransmittableThreadLocal<>();
static ExecutorService executors = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(2));
再次运行效果
RemoteService2:SaltIce-0
RemoteService2:SaltIce-2
RemoteService2:SaltIce-3
RemoteService2:SaltIce-1
RemoteService2:SaltIce-4
RemoteService2:SaltIce-6
RemoteService2:SaltIce-7
RemoteService2:SaltIce-8
RemoteService2:SaltIce-9
RemoteService2:SaltIce-5
这样就可以比较完美的解决线程中、线程池中的ThreadLocal数据的传递问题了。像Zuul与Hystrix如果使用了线程池,需要ThreadLocal进行数据传递的话,就可以通过类似这样的方法去解决了。
最后
以上就是热心保温杯为你收集整理的SpringCloud中Hystrix选择线程池进行隔离时导致的ThreadLocal数据丢失的解决方法参考的全部内容,希望文章能够帮你解决SpringCloud中Hystrix选择线程池进行隔离时导致的ThreadLocal数据丢失的解决方法参考所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复