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

Dav*_*ock 42 java concurrency thread-local threadpool

我使用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();
    }
}
Run Code Online (Sandbox Code Playgroud)

我这样做是出于正当的原因 - 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责备(或我班级的用户)做正确的事.

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

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

但该怎么办呢?

这里做什么是正确的?

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

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

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

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

Dav*_*ock 31

叹了口气,这是个老消息

嗯,这个派对有点晚了.2007年10月,Josh Bloch(java.lang.ThreadLocalDoug Lea的合着者)写道:

"线程池的使用需要极其谨慎.线程池的粗略使用与线程本地的粗略使用相结合可能导致意外的对象保留,正如许多地方所指出的那样."

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

"性能的每线程实例.Aaron的SimpleDateFormat示例(上图)就是这种模式的一个例子."

一些教训

  1. 如果将任何类型的对象放入任何对象池中,则必须提供一种"稍后"删除它们的方法.
  2. 如果你'使用' ThreadLocal,你可以选择这样做.或者:a)您知道的是,Thread(S),你把值将终止时,您的应用程序完成; 或者b)您可以稍后安排 调用ThreadLocal #set()的相同线程,以便在应用程序终止时调用ThreadLocal #remove()
  3. 因此,将ThreadLocal用作对象池将对应用程序和类的设计造成沉重的代价.好处不是免费的.
  4. 因此,使用ThreadLocal可能是一个不成熟的优化,即使Joshua Bloch敦促您在"Effective Java"中考虑它.

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

注意:ThreadLocal除了"对象池"之外还有其他用途,这些课程不适用于ThreadLocal只是临时设置的情况,或者存在真正的每线程状态的情况踪迹.

图书馆实施者的后果

Threre是库实现者的一些后果(即使这些库是项目中的简单实用程序类).

或者:

  1. 你使用ThreadLocal,完全意识到你可能会因为额外的行李而"污染"长时间运行的线程.如果您正在实施java.util.concurrent.ThreadLocalRandom,那可能是合适的.(如果你没有实现,Tomcat可能仍会对你的库的用户抱怨java.*).有趣的是要注意java.*使用ThreadLocal技术的规则.

要么

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

但是,让你的图书馆"难以正常使用".

要么

  1. 您为客户提供了提供自己的对象池实例(可能使用ThreadLocal或某种同步)的机会.("好吧,new ExpensiveObjectFactory<T>() { public T get() {...} }如果你认为它真的是必要的话,我可以给你一个".

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

要么

  1. 你决定对你的应用程序来说这不值得,并找到一种不同的方法来解决问题.那些昂贵的,可变的,非线程安全的对象会让你感到痛苦......使用它们真的是最好的选择吗?

备择方案

  1. 常规对象池,具有所有竞争同步.
  2. 不汇集对象 - 只需在本地范围内实例化它们并稍后丢弃.
  3. 不汇集线程(除非你可以在你喜欢的时候安排清理代码) - 不要在JaveEE容器中使用你的东西
  4. 线程池,它足够聪明,可以清理ThreadLocals,而不会对你产生任何影响.
  5. 线程池,它在"每个应用程序"的基础上分配线程,然后在应用程序停止时让它们死掉.
  6. 线程池容器和应用程序之间的协议,允许注册"应用程序关闭处理程序",容器可以安排在已经用于服务应用程序的线程上运行......在将来的某个时候,该线程是下一个可用.例如.servletContext.addThreadCleanupHandler(new Handler() {@Override cleanup() {...}})

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

Bootnote

实际上,a的实例化GregorianCalendar非常轻量级.这是不可避免的召唤setTime(),导致大部分工作.它也不会在线程执行的不同点之间保持任何重要状态.把一个Calendar成一个ThreadLocal不可能给你回超过它的成本你...除非绝对分析显示了热点new GregorianCalendar().

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


Ale*_*dov 4

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

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

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