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

我使用ThreadLocal

在我的Java类中,我有时使用ThreadLocal作为避免不必要的对象创build的手段:

 @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环境就是一个很好的例子。

事实上,当webapp停止时,Tomcat 7就像这样响起:

SEVERE:Web应用程序[]使用[org.apache.xmlbeans.impl.store.CharUtil $ 1](value [org.apache.xmlbeans.impl.store.CharUtil$1@2aace7a7])types的键创build了一个ThreadLocal,types为[java.lang.ref.SoftReference](值为[java.lang.ref.SoftReference@3d9c9ad4]),但在Web应用程序停止时未能将其删除。 线程将会随着时间的推移而被更新,以避免可能的内存泄漏。 2012年12月13日12:54:30 org.apache.catalina.loader.WebappClassLoader checkThreadLocalMapForLeaks

(即使我的代码,在这种情况下)。

谁应该责怪?

这似乎不公平。 Tomcat责怪 (或我class的用户)做正确的事情。

最终,这是因为Tomcat希望重新使用它提供给我的线程,用于其他 Web应用程序。 (呃,我觉得很肮脏。)对于Tomcat来说,这可能不是一个很好的策略 – 因为线程实际上具有/导致状态 – 在应用程序之间不共享它们。

但是,这个政策至less是普遍的,即使这是不可取的。 我觉得作为一个ThreadLocal用户,我有义务为我的类提供一种方式来“释放”我的类附加到各个线程的资源。

但是该怎么办?

这里做什么是正确的?

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

但是,也许我应该提供一个设施,允许用户说出“与这个class级相关的粗暴的线程状态,尽pipe我不能让线程死掉,让GC做它的事情?”。 我能做到这一点吗? 我的意思是,我不能安排ThreadLocal#remove()在过去的某个时间看到ThreadLocal#initialValue()每个线程上调用。 还是有另一种方式?

还是应该对用户说:“让自己一个体面的类加载器和线程池实现”?

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

叹息,这是老消息

好吧,这个晚会晚了一点。 在2007年10月,Josh Bloch(与Doug Lea一起的java.lang.ThreadLocal合着者) 写道 :

“线程池的使用需要非常小心,在许多地方都已经注意到,使用线程池并结合使用粗糙的线程本地语言,会导致意外的对象保留。

人们抱怨ThreadLocal与线程池之间的交互不良。 但乔希做了批准:

性能的每线程实例,上面的Aaron的SimpleDateFormat示例就是这种模式的一个例子。“

一些教训

  1. 如果您将任何types的对象放入任何对象池中,则必须提供一种“稍后”删除的方法。
  2. 如果你使用ThreadLocal “池”,你有这样做的select有限。 或者:a)您知道 ,当您的应用程序完成时,您放置值的Thread将终止; 或b)稍后您可以安排调用ThreadLocal#set()来调用ThreadLocal#remove(),每当您的应用程序终止
  3. 因此,使用ThreadLocal作为对象池将会严重影响应用程序和类的devise。 好处不是免费的。
  4. 因此,使用ThreadLocal可能是一个不成熟的优化,尽pipeJoshua Bloch强烈build议您考虑使用“Effective Java”。

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

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

对图书馆执行者的后果

Threre对于图书馆执行者来说是一些后果(即使这些图书馆是你项目中的简单实用程序类)。

或者:

  1. 你使用ThreadLocal,完全知道你可能'污染'额外的行李长时间运行的线程。 如果你正在实现java.util.concurrent.ThreadLocalRandom ,那可能是合适的。 (如果你没有在java.*实现的话,Tomcat可能仍然会对你的库的用户抱怨)。 有趣的是注意到java.*使得使用ThreadLocal技术的原则。

要么

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

但是,使你的图书馆很难正确使用。

要么

  1. 你给你的客户机会提供自己的对象池推进(可能使用ThreadLocal,或某种types的同步)。 (“好吧,如果你认为这真的是坏事,我可以给你一个new ExpensiveObjectFactory<T>() { public T get() {...} }

不是那么糟糕。 如果对象真的非常重要,而且创build成本很高,那么明确的共享可能是值得的。

要么

  1. 你认为这对你的应用来说并不值得,而是find一种不同的方法来解决这个问题。 那些昂贵的创build,可变的,非线程安全的对象正在造成你的痛苦…无论如何,使用它们真的是最好的select?

备择scheme

  1. 定期的对象池,所有的竞争同步。
  2. 不合并对象 – 只是在本地范围内实例化它们,稍后放弃。
  3. 不汇集线程(除非你可以安排时间清理代码) – 不要在JaveEE容器中使用你的东西
  4. 线程池足够聪明,可以清理ThreadLocals而不用嘲笑你。
  5. 线程池在“每个应用程序”基础上分配线程,然后在应用程序停止时让它们死掉。
  6. 线程池容器和允许注册“应用程序closures处理程序”的应用程序之间的协议,容器可以调度在应用程序服务的线程上运行…在将来的某个时刻,下一个可用。 例如。 servletContext.addThreadCleanupHandler(new Handler() {@Override cleanup() {...}})

在未来的JavaEE规范中,最近看到最近3个项目的标准化会很高兴。

Bootnote

实际上, GregorianCalendar实例化非常轻巧。 这是setTime()大部分工作的不可避免的要求。 在线程的不同点之间也不存在任何显着的状态。 把一个Calendar放到一个ThreadLocal中不太可能让你回到比你花费更多的地方,除非在new GregorianCalendar()分析确实显示了一个热点。

new SimpleDateFormat(String)比较昂贵,因为它必须parsing格式string。 一旦被parsing,对象的“状态”对于以后由相同的线程使用是重要的。 这是一个更好的契合。 但是实例化一个新的实例可能还是“更便宜”,而不是给你的课程额外的责任。

由于线程不是由您创build的,因此只能由您租用,我认为在停止使用之前要求清理它是公平的 – 就像您在返回时填充租用的汽车的油箱一样。 Tomcat可以自己清理所有的东西,但是这对你来说是个好消息,提醒你忘记了东西。

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

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

如果有任何帮助,我使用自定义SPI(一个接口)和JDK ServiceLoader 。 然后我需要做的threadlocals卸载的所有我的各种内部库(jar)只是按照ServiceLoader模式。 所以如果一个jar需要threadlocal清理,它会自动获取,如果它有适当的/META-INF/services/interface.name

然后,我在卸载filter或侦听器(我有听众的问题,但我不记得是什么)卸载。

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

我认为JDK的ThreadPoolExecutor可以在任务执行后执行ThreadLocals清理,但正如我们所知道的那样。 我认为它可以提供至less一个选项。 之所以可能是因为Thread只提供对其TreadLocal映射的包私有访问,所以ThreadPoolExecutor只是无法在不更改Thread的API的情况下访问它们。

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

经过一年的思考之后,我决定JavaEE容器不能在不相关应用程序的实例之间共享池工作线程。 这根本不是“企业”。

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

java.lang命名空间中进行这样的修改之前,应用程序部署者只能采用“一个线程池,相关应用程序的一个实例”规则,而对于应用程序开发人员来说,这个线程是我的,直到ThreadDeath部分'。