当类暴露于线程池时,清理 ThreadLocal 资源真的是我的工作吗?

Posted

技术标签:

【中文标题】当类暴露于线程池时,清理 ThreadLocal 资源真的是我的工作吗?【英文标题】:Is it really my job to clean up ThreadLocal resources when classes have been exposed to a thread pool? 【发布时间】:2012-11-30 22:00:31 【问题描述】:

我对 ThreadLocal 的使用

在我的 Java 类中,我有时使用 ThreadLocal 主要是为了避免不必要的对象创建:

@net.jcip.annotations.ThreadSafe
public class DateSensitiveThing 

    private final Date then;

    public DateSensitiveThing(Date then) 
        this.then = then;
    

    private static final ThreadLocal<Calendar> threadCal = new ThreadLocal<Calendar>()   
        @Override
        protected Calendar initialValue() 
            return new GregorianCalendar();
        
    ;

    public Date doCalc(int n) 
        Calendar c = threadCal.get();
        c.setTime(this.then):
        // use n to mutate c
        return c.getTime();
    

我这样做是有正当理由的——GregorianCalendar 是那些光荣的有状态、可变、非线程安全的对象之一,它提供跨多个调用的服务,而不是代表一个值。此外,实例化被认为是“昂贵的”(这是否正确不是这个问题的重点)。 (总的来说,我真的很佩服它:-))

Tomcat 如何发牢骚

但是,如果我在任何池线程的环境中使用这样的类 - 并且 我的应用程序无法控制这些线程的生命周期 - 那么就有可能发生内存泄漏。 Servlet 环境就是一个很好的例子。

事实上,当 webapp 停止时,Tomcat 7 会这样抱怨:

严重:Web 应用程序 [] 创建了一个具有类型键的 ThreadLocal [org.apache.xmlbeans.impl.store.CharUtil$1](价值 [org.apache.xmlbeans.impl.store.CharUtil$1@2aace7a7]) 和值为 类型 [java.lang.ref.SoftReference] (值 [java.lang.ref.SoftReference@3d9c9ad4])但未能删除它时 Web 应用程序已停止。线程将被更新 是时候尝试避免可能的内存泄漏了。 2012 年 12 月 13 日 12:54:30 下午 org.apache.catalina.loader.WebappClassLoader checkThreadLocalMapForLeaks

(在这种特殊情况下,甚至我的代码都没有这样做)。

谁是罪魁祸首?

这似乎不太公平。 Tomcat 指责 me(或我班的用户)做了正确的事情。

最终,这是因为 Tomcat 想要将它提供给我的线程重用于其他网络应用程序。 (呃 - 我觉得很脏。)可能,这对 Tomcat 来说不是一个很好的策略 - 因为线程实际上确实有/导致状态 - 不要在应用程序之间共享它们。

然而,这个政策至少是普遍的,即使它是不可取的。我觉得我有义务 - 作为ThreadLocal 用户,为我的班级提供一种方法来“释放”我的班级附加到各个线程的资源。

但是该怎么办呢?

在这里做什么是正确的?

在我看来,servlet 引擎的线程重用策略似乎与 ThreadLocal 背后的意图不一致。

但也许我应该提供一个工具让用户说“开始,与这个类相关的邪恶线程特定状态,即使我无法让线程死亡并让 GC 做它的事情?”。我有可能做到这一点吗?我的意思是,我不能安排在过去某个时间看到ThreadLocal#initialValue() 的每个线程上调用ThreadLocal#remove()。还是有其他方法?

或者我应该对我的用户说“去给自己找一个像样的类加载器和线程池实现”?

EDIT#1:阐明了 threadCal 如何在不了解线程生命周期的 vanailla 实用程序类中使用 EDIT#2:修复了DateSensitiveThing 中的线程安全问题

【问题讨论】:

***.com/questions/321757/… @schtever Erm,我知道肯定不会使用 ThreadLocal 将每个请求的信息存储在 Servlet 中。但是,使用它们还有其他原因,而且它们与 servlet 引擎的交互仍然很差。我的问题是关于如何处理它,而不是是否应该这样做。 【参考方案1】:

由于线程不是你创建的,它只是你租的,我认为在停止使用之前要求清理它是公平的 - 就像你在返回时加满租来的汽车的油箱一样。 Tomcat 可以自己清理所有内容,但它会帮您一个忙,提醒您忘记的东西。

添加: 您使用准备好的 GregorianCalendar 的方式是完全错误的:由于服务请求可以是并发的,并且没有同步,doCalc 可以采用 getTime ater setTime 由另一个请求调用。引入同步会使事情变慢,因此创建一个新的GregorianCalendar 可能是一个更好的选择。

换句话说,您的问题应该是:如何保持准备好的GregorianCalendar 实例池,以便根据请求率调整其数量。因此,至少,您需要一个包含该池的单例。每个 Ioc 容器都有管理单例的方法,并且大多数都有现成的对象池实现。如果您还没有使用 IoC 容器,请开始使用一个(String、Guice),而不是重新发明***。

【讨论】:

有什么想法我的DateSensitiveThing实际上可以清理吗? 好的,我修复了代码,现在它真的是线程安全的(doCalc 中的多个线程现在很好)。不过你有一个好点子 - 我基本上使用“ThreadLocal”作为一个简单(高效、低争用)对象池,但我不愿意“付出代价”并管理我放入该池的对象的生命周期。【参考方案2】:

我认为 JDK 的 ThreadPoolExecutor 可以在任务执行后进行 ThreadLocals 清理,但我们知道它不会。我认为它至少可以提供一个选择。原因可能是因为 Thread 仅提供对其 TreadLocal 映射的包私有访问,因此 ThreadPoolExecutor 无法在不更改 Thread 的 API 的情况下访问它们。

有趣的是,ThreadPoolExecutor 具有受保护的方法存根 beforeExecutionafterExecution,API 说:These can be used to manipulate the execution environment; for example, reinitializing ThreadLocals...。所以我可以想象一个实现 ThreadLocalCleaner 接口的 Task 和我们自定义的 ThreadPoolExecutor,在 afterExecution 调用任务的 cleanThreadLocals();

【讨论】:

当我控制启动和停止 Thread 时,afterExecution 钩子是有意义的。但是,当我是 servlet 容器中的租户时,我无法(也不应该)控制池中线程的生命周期。使用 ThreadLocal 的代码假定当前正在执行的线程由当前正在执行的应用程序拥有。在 servlet 容器中,不幸的是,这不是真的。【参考方案3】:

叹息,这是旧闻

好吧,这次聚会有点晚了。 2007 年 10 月,Josh Bloch(java.lang.ThreadLocal 和 Doug Lea 的合著者)wrote:

“线程池的使用需要格外小心。马虎地使用线程 池与对线程局部变量的草率使用可能导致 正如许多地方所指出的那样,意外的对象保留。”

即使在那时,人们也抱怨 ThreadLocal 与线程池的不良交互。但乔希确实认可了:

“提高性能的每线程实例。Aaron 的 SimpleDateFormat 示例(上图)就是这种模式的一个示例。”

一些教训

    如果您将任何类型的对象放入任何对象池中,您必须提供一种“稍后”删除它们的方法。 如果您使用ThreadLocal 进行“池化”,那么您的选择有限。任何一个: a) 您知道当您的应用程序完成时,您放置值的Thread(s) 将终止;要么 b) 您可以稍后安排调用 ThreadLocal#set() 的 同一线程 在应用程序终止时调用 ThreadLocal#remove() 因此,将 ThreadLocal 用作对象池将给应用程序和类的设计带来沉重的代价。这些好处不是免费的。 因此,使用 ThreadLocal 可能是一种过早的优化,尽管 Joshua Bloch 敦促您在“Effective Java”中考虑它。

简而言之,决定使用 ThreadLocal 作为对“每个线程实例池”的快速、无竞争访问的一种形式并不是一个轻率的决定。

注意:除了“对象池”之外,ThreadLocal 还有其他用途,这些课程不适用于那些 ThreadLocal 只是临时设置的场景,或者存在真正的每个线程的场景要跟踪的状态。

库实施者的后果

库实现者会产生一些后果(即使此类库是您项目中的简单实用程序类)。

要么:

    您使用 ThreadLocal,完全意识到您可能会“污染”长时间运行的线程并带来额外的负担。如果您正在实施java.util.concurrent.ThreadLocalRandom,它可能是合适的。 (如果您没有在java.* 中实现,Tomcat 可能仍然会抱怨您的库的用户)。值得注意的是 java.* 谨慎使用 ThreadLocal 技术的原则。

    您使用 ThreadLocal,并为您的类/包的客户端提供: a) 选择放弃优化的机会(“不要使用 ThreadLocal ...我无法安排清理”);和 b) 一种清理 ThreadLocal 资源的方法(“使用 ThreadLocal 没问题……我可以安排所有使用你调用 LibClass.releaseThreadLocalsForThread() 的线程,当我完成它们时。

但是,使您的库“难以正确使用”。

    您让您的客户有机会提供他们自己的对象池实现(可能使用 ThreadLocal 或某种同步)。 (“好的,如果你认为真的有必要,我可以给你new ExpensiveObjectFactory&lt;T&gt;() public T get() ... ”。

还不错。如果对象真的那么重要并且创建起来那么昂贵,那么显式池可能是值得的。

    无论如何,您认为它对您的应用程序没有那么大的价值,并找到解决问题的不同方法。那些创建成本高、可变、非线程安全的对象让你很痛苦......无论如何,使用它们真的是最好的选择吗?

替代方案

    常规对象池,以及所有竞争同步。 不池化对象 - 只需在本地范围内实例化它们并稍后丢弃。 不合并线程(除非您可以根据需要安排清理代码)- 不要在 JaveEE 容器中使用您的东西 足够聪明的线程池可以清理 ThreadLocals 而不会对您发脾气。 线程池以“每个应用程序”为基础分配线程,然后在应用程序停止时让它们消亡。 线程池容器和应用程序之间的协议,它允许注册“应用程序关闭处理程序”,容器可以安排它在用于服务应用程序的线程上运行......在将来的某个时候,当该线程是下一个可用的。例如。 servletContext.addThreadCleanupHandler(new Handler() @Override cleanup() ...)

很高兴在未来的 JavaEE 规范中看到围绕最后 3 项的一些标准化。

引导说明

实际上,GregorianCalendar 的实例化非常轻量级。这是对setTime() 的不可避免的调用,这导致了大部分工作。它也不会在线程执行的不同点之间保持任何重要状态。将Calendar 放入ThreadLocal 不太可能给您带来比您付出的更多的回报……除非分析明确显示new GregorianCalendar() 中的热点。

new SimpleDateFormat(String) 相比之下比较昂贵,因为它必须解析格式字符串。解析后,对象的“状态”对于同一线程以后的使用很重要。这是更合适的。但是实例化一个新的可能仍然比给你的类额外的责任“更便宜”。

【讨论】:

【参考方案4】:

在考虑了一年之后,我认为 JavaEE 容器在不相关的应用程序实例之间共享池工作线程是不可接受的。这根本不是“企业”。

如果你真的要共享线程,java.lang.Thread(至少在 JavaEE 环境中)应该支持像setContextState(int key)forgetContextState(int key)(镜像setClasLoaderContext())这样的方法,它们允许容器隔离特定于应用程序的 ThreadLocal 状态,因为它在各种应用程序之间处理线程。

等待java.lang命名空间中的此类修改,只有应用程序部署者采用“一个线程池,相关应用程序的一个实例”规则是明智的,并且应用程序开发人员假设“这个线程是我的,直到ThreadDeath we do part'。

【讨论】:

实现所需的唯一方法是让 Tomcat 为每个 Web 应用程序提供一个单独的线程池。我不知道有一个 servlet 容器可以做到这一点,我也不认为这是一个好主意,尤其是考虑到热部署和多战部署通常已死(即 spring-boot 和 dropwizard uber jar 正在成为常态)。一般来说,它也几乎是不可能的,因为必须对请求进行初始处理才能确定要分派到哪个 webapp(战争)...... @Adam 是的,在一个应用容器中托管多个应用的​​整个概念根本不是 Java EE 能够通过任何类型的资源使用安全提供的东西。 (这对最初用于销售 Java EE 的整个 概念 是一个不利因素)。 Tomcat 的方法(随着时间的推移检测泄漏和释放线程池是容器真正可以做的所有事情。 最后,我认为这意味着 库开发人员 应该提供一个 API 来清理之前提交给库的线程,但是用户现在想要“清理”。 Java EE 规范的未来版本应该 规定应用程序容器应该 提供关闭挂钩,以便“以前提供给该应用程序的所有线程”都提供给应用程序本身用于清理。 我正是为自己的内部库做到了这一点。看我的回答:***.com/a/28945239/318174【参考方案5】:

如果有任何帮助,我会使用自定义 SPI(接口)和 JDK ServiceLoader。然后我所有需要卸载threadlocals的各种内部库(jar)都遵循ServiceLoader模式。因此,如果 jar 需要 threadlocal 清理,如果它具有适当的 /META-INF/services/interface.name,它将自动被选中。

然后我在过滤器或侦听器中进行卸载(我在侦听器方面遇到了一些问题,但我不记得是什么)。

如果 JDK/JEE 带有用于清除 threadlocals 的标准 SPI,那将是理想的。

【讨论】:

好的,我明白使用ServiceLoader 可以帮助获得ThreadLocalScrubberService,如果图书馆作者承诺提供一个,那么框架作者 可以使用它。 (为此,您介意发布您的服务接口吗?)我不明白的是,作为 应用程序作者,您如何能够哄骗 servlet 容器来安排线程上的清理工作哪些需要清洗?或者您是否在每个 HTTP 请求后都刻意擦洗,谴责库重新初始化其 ThreadLocals,为了安全起见从本质上阻碍了性能? 为了安全起见,宾果游戏...如果您使用 threadlocals 进行对象池、缓存或因为您的数据结构不是线程安全的 IMO,那么您做错了。你应该停止使用任何库......除了一些例外是ThreadLocalRandom。我们使用ThreadLocals 将上下文向下传递到请求或离开消息总线。所以清理实际上是为了防止意外的上下文重用,但也恰好是为了防止你的问题。

以上是关于当类暴露于线程池时,清理 ThreadLocal 资源真的是我的工作吗?的主要内容,如果未能解决你的问题,请参考以下文章

在使用线程池时应特别注意对ThreadLocal的使用

ThreadLocal遇到线程池时, 各线程间的数据会互相干扰, 串来串去

使用线程池时,多线程之间上下文参数传递失效解决办法

使用线程池时,多线程之间上下文参数传递失效解决办法

清理ThreadLocal

如何避免忘记清理 ThreadLocal ?