SpringCloud中Hystrix选择线程池进行隔离时导致的ThreadLocal数据丢失的解决方法参考

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进行数据传递的话,就可以通过类似这样的方法去解决了。

猜你喜欢

转载自blog.csdn.net/qq_41257365/article/details/108767690