在我的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
是那些光荣的有状态,可变,非线程安全的对象之一,它提供跨多个调用的服务,而不是表示值。此外,它被认为是“昂贵的”实例化(这是否真实不是这个问题的重点)。 (总的来说,我真的很佩服它: - ))
但是,如果我在任何聚集线程的环境中使用这样的类 - 和我的应用程序无法控制这些线程的生命周期 - 那么就有可能发生内存泄漏。 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
答案 0 :(得分:30)
嗯,这次派对有点晚了。 2007年10月,Josh Bloch(java.lang.ThreadLocal
和Doug Lea的共同作者)wrote:
&#34;使用线程池需要特别小心。邋use使用线程 池与本地线程的粗心使用相结合可能会导致 非预期的物体保留,正如许多地方所指出的那样。&#34;
人们抱怨ThreadLocal与线程池的错误交互,即便如此。但乔希做了制裁:
&#34;性能的每线程实例。 Aaron的SimpleDateFormat示例(上图)就是这种模式的一个例子。&#34;
ThreadLocal
,您可以选择这样做。或者:
a)您知道在您的申请完成后,您放置值的Thread
将终止;要么
b)您可以在以后安排相同的线程调用ThreadLocal #set()以在应用程序终止时调用ThreadLocal #remove()简而言之,决定使用ThreadLocal作为对每个线程实例池的快速,无竞争访问的形式&#34;不是轻率的决定。
注意:ThreadLocal除了&#39;对象池之外还有其他用途,这些课程不适用于ThreadLocal只打算临时设置的场景,或者那里是真正的每线程状态来跟踪。
Threre对库实现者有一些影响(即使这些库是项目中的简单实用程序类)。
或者:
java.util.concurrent.ThreadLocalRandom
,则可能是合适的。 (如果您未在java.*
中实施,Tomcat可能仍会对您图书馆的用户抱怨。值得注意的是java.*
使用ThreadLocal技术的规则。OR
LibClass.releaseThreadLocalsForThread()
。< / LI>
醇>
让您的图书馆难以正常使用&#39;但是。
OR
new ExpensiveObjectFactory<T>() { public T get() {...} }
。没那么糟糕。如果对象真的那么重要并且创建起来很昂贵,那么显式池化可能是值得的。
OR
servletContext.addThreadCleanupHandler(new Handler() {@Override cleanup() {...}})
很高兴在未来的JavaEE规范中看到最后3个项目的标准化。
实际上,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具有受保护的方法存根beforeExecution
和afterExecution
,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我们分开'。