【问题标题】:Preserving threadlocal beans while kicking off mulitple threads in Spring在 Spring 中启动多个线程时保留 threadlocal bean
【发布时间】:2014-07-25 23:31:40
【问题描述】:

在传统设置中,我的应用程序做一些简单的事情:接受请求,加载与发送该请求的特定用户相关的一些元数据(来自繁重的 restful 调用),并将此元数据存储在线程本地 bean 上下文中(即在一个简单的 hashMap 支持到上下文的类中),这样只要我注入 bean,我就可以从代码路径中的任何其他类轻松访问这些详细信息。这与在我的 API 生命周期中注入的所有其他非线程本地 bean 和谐地工作,因此没有任何问题。

但是,一旦我尝试做一些聪明的事情(在单独的线程中启动一些任务以模仿“即发即弃”的调用),我注意到无论代码路径(在这个新线程中)依赖于threadlocal bean 我被烧毁了,因为之前保存的 hashMap(烘焙到上下文的类中)的细节现在已经消失了。对我来说,它们为什么消失是完全有道理的,因为毕竟,一旦我启动了一个新线程,它并没有自动将父线程的上下文“克隆”到新子线程的上下文中。

我的问题是,我可以利用任何现有的 spring 框架机制(或纯 Java)来确保为我启动的新子线程保留这些信息?将原始父线程的上下文“深度克隆”到新子线程中有多简单?如果这是一种天真的方法,有人可以推荐一些我可以在启动线程之前进行的其他设置吗?鉴于我只想为请求的范围维护此上下文,我不确定是否可以使用something like Spring's object pooling(毕竟,我不希望出现新请求进入并回收不同/旧的情况用户对新用户的偏好)。

【问题讨论】:

    标签: java multithreading spring


    【解决方案1】:

    我看到了三种工作方式。

    使用'InheritableThreadLocal`

    这是ThreadLocal 的子类和默认的JDK 功能。 可继承的线程局部变量会自动从其父线程复制到子线程中。 似乎是个不错的候选人。
    已编辑:如果您的代码看起来像这样,那么它就是一个替代品。

    public class Test {
    
    // I guess this, in your actual code, is an injected Reference
    InheritableThreadLocal<String> expensiveData = new InheritableThreadLocal<String>();
    
    public void work(String userName){
        expensiveData.set(computeExpensiveData(userName)); // whatever that is
    
        Worker workUnit = new Worker();
        workUnit.userData = expensiveData; // Spring does some variant of this (I guess)
    
        Thread child = new Thread(workUnit); // This is the key line
        child.start(); // As long as the parent thread has a clean "expensiveData"
                       // The child thread will have too.
    }
    
    protected static class Worker implements Runnable {
        // This is injected too...
        protected InheritableThreadLocal<String> userData = null;
        @Override
        public void run() {
            userData.get(); // Returns spwaning thread's version
            // Work ...!
            return;
        }
    }
    }
    

    如果Thread 实例(此处为child 变量)未在此处创建或启动,而是在池中重用,则存在从WorkeruserData 中产生错误值的实际风险.

    使用请求/会话范围的 bean

    如果您访问数据的方式是通过某种 bean,也许您可​​以将此 bean 的生命周期与您的 webapp 的会话或请求联系起来。每次你都会自动获得一个新的 bean,但你会在你的范围内保留同一个 bean。

    重构为缓存

    Spring 具有@Cachable 注释,可以合理地轻松缓存给定的方法调用。再一次,您可以将对数据的访问设置为 @Cachable,并将缓存配置调整为“每个用户”。
    编辑:Spring 缓存使用代理,所以它是一个正在工作的方法级拦截。 我称其为 reafactor,因为它需要您做一些工作,但在我看来这是一种更清洁、更像弹簧的处理方式。从与上面相同的示例开始,主要区别在于您必须创建一个新的 Bean,该 Bean 将实现:

    public static interface ExpensiveUserDataCalculator {
        @Cacheable // With the proper configuration tuning (eviction ? TTL ? size ?)
        public String computeExpensiveData(String userName);
    }
    

    实现只是一个细节(您可能会从当前代码中复制/粘贴它)。 这个 bean 将被注入(用 Spring 的术语来说,它是 collaborator)。

    这看起来像这样。请注意,您不必操作 ThreadLocal 或生成线程或其他任何东西。只需让每个协作者完成其工作,将它们与 Spring 连接在一起,然后使用 @Cacheable(或您认为合适的其他缓存方式)进行优化。缓存是您工作流程的“实施细节”。

    // This is a thread pool, or whatever...
    protected Object workDispatcher = null;
    
    public void work(String userName){
        Worker workUnit = new Worker(userName); // Setup
        workDispatcher.dispatch(workUnit);
    }
    
    protected static class Worker implements Runnable {
        // This is injected Injected in you situation (?)
        protected ExpensiveUserDataCalculator userDataProvider = null;
        protected String userName = null;
        public Worker(String userName) { this.userName = userName; }
        @Override
        public void run() {
            userDataProvider.computeExpensiveData(userName); 
            // Work ...!
            return;
        }
    }
    

    最后两个方法在 Spring 参考指南中有自己的部分,因此如果需要,请查看。

    【讨论】:

    • 澄清一下,使用 InheritableThreadLocal 不会让线程池中的线程被回收,从而导致信息泄露?至于可缓存注释,而不是使方法调用可缓存,我是否可以使类的属性可缓存(因此在我的示例中,我将设置 5 个不同的属性,其中一个依赖于方法调用)?
    猜你喜欢
    • 2023-03-06
    • 2013-01-19
    • 2017-01-02
    • 1970-01-01
    • 1970-01-01
    • 2023-02-01
    • 1970-01-01
    • 2023-02-11
    • 2017-01-27
    相关资源
    最近更新 更多