• 异步线程本地变量丢失问题


    每个用户请求进入服务,我们使用拦截器做一些前置处理,譬如查询用户的个人信息,将结果保存到线程本地变量中。在整个请求中,都能随时从堆缓存中拿到这部分信息。

    相信大家也经常使用这种办法,但是某次遇到一个bug,那就是在主线程中使用异步线程去查询其他系统的信息,而异步线程是没有存这个本地变量的!结果喜闻乐见NPE。

    当时是将变量作为异步线程的任务参数手动传递进去,后来看《亿级流量》,学到一种技术,可以在异步线程执行之前,将缓存注入到异步线程中,即多个线程共用一个缓存。

    两个概念

    线程级别本地缓存:ThreadLocal

    请求级别本地缓存:HystrixRequestContext

    1. 源码分析

    1. HystrixRequestContext(“当铺”)

    public class HystrixRequestContext implements Closeable {
        //这个属性类HystrixRequestContext的所有实例共享,所有线程在自己的ThreadLocalMap中存储各自的HystrixRequestContext时,都以这个作为key
        private static ThreadLocal<HystrixRequestContext> requestVariables = new ThreadLocal<HystrixRequestContext>();
    
        //判断当前线程中是否有HystrixRequestContext
        public static boolean isCurrentThreadInitialized() {
            HystrixRequestContext context = requestVariables.get();
            return context != null && context.state != null;
        }
    
        //获取当前线程中存储的HystrixRequestContext
        public static HystrixRequestContext getContextForCurrentThread() {
            HystrixRequestContext context = requestVariables.get();
            if (context != null && context.state != null) {
                return context;
            } else {
                return null;
            }
        }
    
        //设置当前线程的HystrixRequestContext
        public static void setContextOnCurrentThread(HystrixRequestContext state) {
            requestVariables.set(state);
        }
    
        //初始化当前线程的HystrixRequestContext
        public static HystrixRequestContext initializeContext() {
            HystrixRequestContext state = new HystrixRequestContext();
            requestVariables.set(state);
            return state;
        }
    
        //真正存储变量副本的地方
        /* package */ ConcurrentHashMap<HystrixRequestVariableDefault<?>, HystrixRequestVariableDefault.LazyInitializer<?>> state = new ConcurrentHashMap<HystrixRequestVariableDefault<?>, HystrixRequestVariableDefault.LazyInitializer<?>>();
    
        private HystrixRequestContext() {
    
        }
    
        //使用后需要释放防止内存泄露
        public void shutdown() {
            if (state != null) {
                for (HystrixRequestVariableDefault<?> v : state.keySet()) {
    
                    try {
                        HystrixRequestVariableDefault.remove(this, v);
                    } catch (Throwable t) {
                        HystrixRequestVariableDefault.logger.error("Error in shutdown, will continue with shutdown of other variables", t);
                    }
                }
                state = null;
            }
        }
    
        public void close() {
            shutdown();
        }
    
    }

     2. HystrixRequestVariableDefault(“当票”)

    public class HystrixRequestVariableDefault<T> implements HystrixRequestVariable<T> {
        static final Logger logger = LoggerFactory.getLogger(HystrixRequestVariableDefault.class);
    
        public HystrixRequestVariableDefault() {
        }
    
        @SuppressWarnings("unchecked")
        public T get() {
            if (HystrixRequestContext.getContextForCurrentThread() == null) {
                throw new IllegalStateException(HystrixRequestContext.class.getSimpleName() + ".initializeContext() must be called at the beginning of each request before RequestVariable functionality can be used.");
            }
            //从当前线程的HystrixRequestContext中取出存储的所有变量副本
            ConcurrentHashMap<HystrixRequestVariableDefault<?>, HystrixRequestVariableDefault.LazyInitializer<?>> variableMap = HystrixRequestContext.getContextForCurrentThread().state;
            //取出对应变量的副本
            HystrixRequestVariableDefault.LazyInitializer<?> v = variableMap.get(this);
            if (v != null) {
                return (T) v.get();
            }
    
    
            HystrixRequestVariableDefault.LazyInitializer<T> l = new HystrixRequestVariableDefault.LazyInitializer<T>(this);
            HystrixRequestVariableDefault.LazyInitializer<?> existing = variableMap.putIfAbsent(this, l);
            if (existing == null) {
                return l.get();
            } else {
                return (T) existing.get();
            }
        }
    
        public T initialValue() {
            return null;
        }
    
        //设置变量副本到线程的HystrixRequestContext中
        public void set(T value) {
            HystrixRequestContext.getContextForCurrentThread().state.put(this, new HystrixRequestVariableDefault.LazyInitializer<T>(this, value));
        }
    
        //从当前线程的HystrixRequestContext中移除对应变量
        public void remove() {
            if (HystrixRequestContext.getContextForCurrentThread() != null) {
                remove(HystrixRequestContext.getContextForCurrentThread(), this);
            }
        }
        //...        
    }

     3. HystrixContextRunnable(“中间人”)

    public class HystrixContextRunnable implements Runnable {
    
        private final Callable<Void> actual;
        //父线程的HystrixRequestContext备份
        private final HystrixRequestContext parentThreadState;
    
        public HystrixContextRunnable(Runnable actual) {
            this(HystrixPlugins.getInstance().getConcurrencyStrategy(), actual);
        }
    
        public HystrixContextRunnable(HystrixConcurrencyStrategy concurrencyStrategy, final Runnable actual) {
            this(concurrencyStrategy, HystrixRequestContext.getContextForCurrentThread(), actual);
        }
    
        public HystrixContextRunnable(final HystrixConcurrencyStrategy concurrencyStrategy, final HystrixRequestContext hystrixRequestContext, final Runnable actual) {
            //将任务包装成Callable
            this.actual = concurrencyStrategy.wrapCallable(new Callable<Void>() {
                @Override
                public Void call() throws Exception {
                    actual.run();
                    return null;
                }
            });
            //备份父线程的HystrixRequestContext
            this.parentThreadState = hystrixRequestContext;
        }
    
        @Override
        public void run() {
            //备份子线程的HystrixRequestContext
            HystrixRequestContext existingState = HystrixRequestContext.getContextForCurrentThread();
            try {
                //用父线程的HystrixRequestContext替换给子线程,在任务执行期间,两个线程的HystrixRequestContext一致
                HystrixRequestContext.setContextOnCurrentThread(parentThreadState);
                //
                try {
                    actual.call();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            } finally {
                //还原子线程的HystrixRequestContext
                HystrixRequestContext.setContextOnCurrentThread(existingState);
            }
        }
    
    }

    测试:

    public static void main(String[] args) throws InterruptedException {
        //1
        HystrixRequestContext context = HystrixRequestContext.initializeContext();
        //2.
        final HystrixRequestVariableDefault<String> variableDefault = new HystrixRequestVariableDefault<>();
        //3.
        variableDefault.set("kitty");
        //4.
        HystrixContextRunnable runnable = new HystrixContextRunnable(() -> System.out.println(variableDefault.get()));
        //5.
        new Thread(runnable, "subThread").start();
    }

      第一步:

        创建一个HystrixRequestContext对象H-R-C,主线程在ThreadLocalMap中存储副本,key为HystrixRequestContext的类变量requestVariables,value为H-R-C

      第二步:

        创建一个标记变量(暂且这么称呼吧,通过他从HystrixRequestContext的state属性中换取变量副本)H-R-V-D

      第三步:

        从主线程的ThreadLocalMap中拿出H-R-C,在H-R-C的state属性(一个CHM)中设置变量副本,key为H-R-V-D,value为kitty

      第四步:

        创建一个任务H-C-R,初始化任务时,在H-C-R中记录主线程的H-R-C

      第五步:

        子线程执行任务,调用H-C-R的run方法,在子线程ThreadLocalMap中存储副本,key为HystrixRequestContext的类变量requestVariables,value为父线程的H-R-C

      第六步:

        就是任务中定义的 System.out.println(variableDefault.get()) ,这个会从子线程的ThreadLocalMap中拿出H-R-C,然后在H-R-C的state中找H-R-V-D对应的value即kitty并输出

      有点绕。。

      所有的变量副本都存在HystrixRequestContext的state中,这是一个concurrentHashMap,key是HystrixRequestVariableDefault,value是变量的副本

      而HystrixRequestContext这个每个线程要用的话,会在自己的ThreadLocalMap中存储,key是HystrixRequestContext的类变量requestVariables,value是线程自己创建的HystrixRequestContext

      如何做到子线程共享父线程变量?

      要用HystrixContextRunnable或者HystrixContextCallable,当父线程创建他两时,他们会偷偷的将父线程的HystrixRequestContext藏起来,等子线程执行任务之前,将HystrixRequestContext存到子线程的concurrentHashMap上。注意这里不是拷贝,而是传递的引用,所以父线程和子线程共享的是同一个HystrixRequestContext对象。

    2. 跟ThreadLocal的区别和联系

      1. 一个每个线程独享,另一个父线程和创建的所有子线程共享

      2. ThreadLocal中变量副本存在Thread的ThreadLocalMap中,而HystrixRequestContext将副本存在自己的state中,却将自己存在Thread的ThreadLocalMap中,这样便于将自己整个的分享给其他线程

      3. HystrixRequestContext必须使用HystrixContextRunnable或者HystrixContextCallable来创建线程

      4. HystrixRequestContext的实现依赖ThreadLocal

      

    3. 应用示例

    public class RequestLocalSupport {
    
        private static final HystrixRequestVariableDefault<String> requestLocal = new HystrixRequestVariableDefault<>();
    
        public static String getValue() {
            //当前线程可能没有HystrixRequestContext,所以先初始化一下
            init();
            //从调用线程的HystrixRequestContext中拿变量副本
            return requestLocal.get();
        }
    
        public static void setValue(String value) {
            init();
            requestLocal.set(value);
        }
    
        //每个子线程需要在本地有HystrixRequestContext
        private static void init() {
            if (!HystrixRequestContext.isCurrentThreadInitialized()) {
                HystrixRequestContext.initializeContext();
            }
        }
    
        public static void destroy() {
            if (HystrixRequestContext.isCurrentThreadInitialized()) {
                HystrixRequestContext.getContextForCurrentThread().shutdown();
            }
        }
    
    }
    public static void main(String[] args) {
    
        //假设这是切面或前置处理完成的用户信息预存
        RequestLocalSupport.setValue("user info");
    
        HystrixContextRunnable runnable1 = new HystrixContextRunnable(() -> {
            //任务1 用用户信息查订单
            System.out.println(RequestLocalSupport.getValue());
        });
    
        HystrixContextRunnable runnable2 = new HystrixContextRunnable(() -> {
            //任务2 用用户信息查支付记录
            System.out.println(RequestLocalSupport.getValue());
        });
    
        new Thread(runnable1).start();
        new Thread(runnable2).start();
    
    
    }

    参考:

    HystrixRequestContext实现Request级别的上下文:https://www.cnblogs.com/2YSP/p/11440700.html

    源码学习:https://blog.csdn.net/myle69/article/details/85013561

    人生就像蒲公英,看似自由,其实身不由己。
  • 相关阅读:
    csp-s考试总结
    一种贪心
    P2463 [SDOI2008]Sandy的卡片
    P3294 [SCOI2016]背单词
    *UOJ#164. 【清华集训2015】V
    P5503 [JSOI2016]灯塔
    *P3515 [POI2011]Lightning Conductor
    P4585 [FJOI2015]火星商店问题
    求最短路必经边
    P1903 [国家集训队]数颜色 / 维护队列
  • 原文地址:https://www.cnblogs.com/walker993/p/14706623.html
Copyright © 2020-2023  润新知