当类已公开到线程池时,清理 ThreadLocal 资源真的是我的工作吗?我对 ThreadLocal 的使用雄猫如何鞭打谁该责怪谁?但是该怎么办呢?叹息,这是旧闻一些教训对库实施者的影响选择引导注释

2022-08-31 20:21:21

我对 ThreadLocal 的使用

在我的Java类中,我有时会主要使用a作为避免不必要的对象创建的手段: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

雄猫如何鞭打

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

事实上,当Web应用程序停止时,Tomcat 7会像这样呜呜呜:

SEVERE: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 应用程序停止时未能将其删除。随着时间的推移,线程将更新,以尝试避免可能的内存泄漏。Dec 13, 2012 12:54:30 PM org.apache.catalina.loader.WebappClassLoader checkThreadLocalMapForLeaks

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

谁该责怪谁?

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

最终,这是因为Tomcat希望重用它提供给我的线程,用于其他Web应用程序。也许,对于Tomcat来说,这不是一个很好的策略 - 因为线程实际上确实具有/导致状态 - 不会在应用程序之间共享它们。

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

但是该怎么办呢?

在这里做什么是正确的?

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

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

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

编辑#1:澄清了如何在不知道线程生命周期的vanailla实用程序类中使用 编辑#2:修复了线程安全问题threadCalDateSensitiveThing


答案 1

叹息,这是旧闻

好吧,这个派对有点晚了。2007年10月,Josh Bloch(与Doug Lea合著)写道java.lang.ThreadLocal

“使用螺纹池需要格外小心。草率地使用线程池并结合使用线程局部变量可能会导致意外的对象保留,正如在许多地方所指出的那样。

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

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

一些教训

  1. 如果将任何类型的对象放入任何对象池中,则必须提供一种“稍后”删除它们的方法。
  2. 如果使用 “pool”,则执行此操作的选项有限。或者:a)您知道放置值的(s)将在应用程序完成后终止;或者 b) 您以后可以安排调用 ThreadLocal#set() 的同一线程在应用程序终止时调用 ThreadLocal#remove()ThreadLocalThread
  3. 因此,使用 ThreadLocal 作为对象池将给应用程序和类的设计带来沉重的代价。好处不是免费的。
  4. 因此,使用ThreadLocal可能是一个过早的优化,即使Joshua Bloch敦促你在“Effective Java”中考虑它。

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

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

对库实施者的影响

对于库实现者来说,这是一些后果(即使这些库是项目中的简单实用程序类)。

也:

  1. 您使用 ThreadLocal,完全知道您可能会用额外的包袱“污染”长时间运行的线程。如果要实现 ,则可能是合适的。(如果您没有在 中实现,Tomcat 可能仍然会对库的用户发牢骚)。有趣的是,请注意谨慎使用 ThreadLocal 技术所遵循的学科。java.util.concurrent.ThreadLocalRandomjava.*java.*

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

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

  1. 您让客户端有机会提供他们自己的对象池约束(可能使用 ThreadLocal 或某种同步)。(“好吧,如果你认为它真的有必要,我可以给你一个”。new ExpensiveObjectFactory<T>() { public T get() {...} }

没那么糟糕。如果对象真的那么重要,而且创建起来成本很高,那么显式池化可能是值得的。

  1. 无论如何,您都认为它对您的应用程序来说并不那么有价值,并找到一种不同的方法来解决问题。那些创建成本高昂,可变,非线程安全的对象正在给您带来痛苦......无论如何,使用它们真的是最佳选择吗?

选择

  1. 常规对象池,及其所有争用同步。
  2. 不池化对象 - 只需在本地范围内实例化它们并在以后丢弃。
  3. 不池化线程(除非您可以在需要时安排清理代码) - 不要在 JaveEE 容器中使用您的内容
  4. 线程池足够智能,可以清理线程Locals而不会对您发牢骚。
  5. 线程池,它以“每个应用程序”为基础分配线程,然后在应用程序停止时让它们死亡。
  6. 线程池容器和应用程序之间的协议,允许注册“应用程序关闭处理程序”,容器可以安排在用于为应用程序提供服务的线程上运行...在将来的某个时候,当该线程下次可用时。例如。servletContext.addThreadCleanupHandler(new Handler() {@Override cleanup() {...}})

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

引导注释

实际上,实例化是相当轻量级的。这是不可避免的调用,它承担了大部分工作。它也不会在线程执行的不同点之间保持任何重要状态。把一个放进去不太可能给你比它花费更多的钱给你...除非性能分析在 中明确显示热点。GregorianCalendarsetTime()CalendarThreadLocalnew GregorianCalendar()

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


答案 2

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

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

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


推荐