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

时间:2012-12-13 03:49:42

标签: java concurrency threadpool thread-local

我使用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 Whinges如何

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

事实上,当一个webapp被停止时,Tomcat 7会像这样发出声音:

  

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

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

谁应该受到责备?

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

归根结底,这是因为Tomcat希望重用它为其他 Web应用程序提供给我的线程。 (呃 - 我感觉很脏。)可能,这对Tomcat而言并不是一个很好的策略 - 因为线程确实有/导致状态 - 不要在应用程序之间共享它们。

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

但该怎么办呢?

在这里做什么是正确的?

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

但是也许我应该提供一个工具来允许用户说“与这个类相关联的邪恶的特定于线程的状态,即使我无法让线程死掉并让GC做它的事情?”。我甚至可以这样做吗?我的意思是,我不能安排在过去的某个时间看到ThreadLocal#remove()的每个主题上调用ThreadLocal#initialValue()。或者还有另一种方式吗?

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

编辑#1 :阐明了如何在一个不知道线程生命周期的vanailla实用程序类中使用threadCal 编辑#2 :修复了DateSensitiveThing

中的线程安全问题

5 个答案:

答案 0 :(得分:30)

叹息,这是旧消息

嗯,这次派对有点晚了。 2007年10月,Josh Bloch(java.lang.ThreadLocal和Doug Lea的共同作者)wrote

  

&#34;使用线程池需要特别小心。邋use使用线程   池与本地线程的粗心使用相结合可能会导致   非预期的物体保留,正如许多地方所指出的那样。&#34;

人们抱怨ThreadLocal与线程池的错误交互,即便如此。但乔希做了制裁:

&#34;性能的每线程实例。 Aaron的SimpleDateFormat示例(上图)就是这种模式的一个例子。&#34;

一些课程

  1. 如果您将任何类型的对象放入任何对象池,您必须提供一种方法来删除它们以后再次使用#。
  2. 如果你在&#39;使用ThreadLocal,您可以选择这样做。或者: a)您知道在您的申请完成后,您放置值的Thread将终止;要么 b)您可以在以后安排相同的线程调用ThreadLocal #set()以在应用程序终止时调用ThreadLocal #remove()
  3. 因此,将ThreadLocal用作对象池将对应用程序和类的设计造成沉重的代价。这些好处不是免费的。
  4. 因此,使用ThreadLocal可能是一个不成熟的优化,即使Joshua Bloch敦促您在“有效Java”中考虑它。
  5. 简而言之,决定使用ThreadLocal作为对每个线程实例池的快速,无竞争访问的形式&#34;不是轻率的决定。

    注意:ThreadLocal除了&#39;对象池之外还有其他用途,这些课程不适用于ThreadLocal只打算临时设置的场景,或者那里是真正的每线程状态来跟踪。

    库实现者的后果

    Threre对库实现者有一些影响(即使这些库是项目中的简单实用程序类)。

    或者:

    1. 您使用ThreadLocal,完全清楚您可能会污染&#39;带有额外行李的长期线程。如果您正在实施java.util.concurrent.ThreadLocalRandom,则可能是合适的。 (如果您未在java.*中实施,Tomcat可能仍会对您图书馆的用户抱怨。值得注意的是java.*使用ThreadLocal技术的规则。
    2. OR

      1. 您使用ThreadLocal,并为您的类/包的客户提供: a)有机会选择放弃优化(&#34;不要使用ThreadLocal ......我不能安排清理&#34;);和 b)一种清理ThreadLocal资源的方法(&#34;它可以使用ThreadLocal ...我可以安排所有使用它的线程在我完成时调用LibClass.releaseThreadLocalsForThread()。< / LI>

        让您的图书馆难以正常使用&#39;但是。

        OR

        1. 您为客户提供了提供自己的对象池实例(可能使用ThreadLocal或某种同步)的机会。 (&#34;好的,如果您认为它真的是必要的话,我可以给你一个new ExpensiveObjectFactory<T>() { public T get() {...} }
        2. 没那么糟糕。如果对象真的那么重要并且创建起来很昂贵,那么显式池化可能是值得的。

          OR

          1. 您认为它对您的应用程序的价值不大,并找到了解决问题的不同方法。那些昂贵的,可变的,非线程安全的对象会让你感到痛苦......无论如何使用它们真的是最好的选择吗?
          2. 替代

            1. 常规对象池,包含所有争用同步。
            2. 不汇集对象 - 只需在本地范围内实例化它们并稍后放弃。
            3. 不汇集线程(除非您可以随时安排清理代码) - 不要在JaveEE容器中使用您的东西
            4. 足够聪明的线程池,可以清理ThreadLocals,而不会对你抱怨。
            5. 在每个应用程序上分配线程的线程池&#39;在应用程序停止时让它们死掉。
            6. 线程池容器和应用程序之间的协议,允许注册“应用程序关闭处理程序”,容器可以安排在已经用于为应用程序提供服务的线程上运行...指出将来该线程可用时。例如。 servletContext.addThreadCleanupHandler(new Handler() {@Override cleanup() {...}})
            7. 很高兴在未来的JavaEE规范中看到最后3个项目的标准化。

              Bootnote

              实际上,GregorianCalendar的实例化非常轻量级。这是setTime()不可避免地要求招致大部分工作的电话。它也不会在线程执行的不同点之间保持任何重要状态。将Calendar放入ThreadLocal不太可能给你带来的回报超过你的成本...除非分析确实显示new GregorianCalendar()中的热点。

              相比之下,

              new SimpleDateFormat(String)是昂贵的,因为它必须解析格式字符串。解析后,状态为&#39;该对象对于后来由同一线程使用是重要的。它更合适。但它可能仍然“便宜”而且价格便宜。实例化一个新的,而不是给你的课程额外的责任。

答案 1 :(得分:3)

由于线程不是由你创建的,它只是由你租用的,我认为在停止使用之前需要清理它是公平的 - 就像你在返回时填满租来的汽车的坦克一样。 Tomcat可以自己清理所有东西,但是它会帮到你,让人想起忘记的东西。

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

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

答案 2 :(得分:1)

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

然后我在过滤器或监听器中进行卸载(我与听众有一些问题,但我不记得是什么)。

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

答案 3 :(得分:0)

我认为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上调用task的cleanThreadLocals();

答案 4 :(得分:0)

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

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

java.lang命名空间中进行此类修改之前,应用程序部署者只能采用“一个线程池,一个相关应用程序实例”规则,并且应用程序开发人员认为'此线程是直到ThreadDeath我们分开'。