48

我对 ThreadLocal 的使用

在我的 Java 类中,我有时ThreadLocal主要使用 a 来避免不必要的对象创建:

@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 是如何发牢骚的

但是,如果我在任何池化线程的环境中使用这样的类——并且我的应用程序无法控制这些线程的生命周期——那么就有可能发生内存泄漏。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 PM org.apache.catalina.loader.WebappClassLoader checkThreadLocalMapForLeaks

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

谁该受责备?

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

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

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

但是该怎么办呢?

在这里做什么是正确的?

在我看来,servlet 引擎的线程重用策略似乎与ThreadLocal.

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

还是我应该对我的用户说“去给自己找一个像样的类加载器和线程池实现”?

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

4

5 回答 5

35

叹息,这是旧闻

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

“线程池的使用需要格外小心。正如在许多地方所指出的那样,随意使用线程池和随意使用线程局部变量会导致意外的对象保留。”

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

“用于性能的每线程实例。Aaron 的 SimpleDateFormat 示例(上图)是这种模式的一个示例。”

一些教训

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

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

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

图书馆实施者的后果

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

任何一个:

  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 项的一些标准化。

引导笔记

实际上, a 的实例化GregorianCalendar非常轻量级。这是不可避免的调用setTime(),导致大部分工作。它也不会在线程执行的不同点之间保持任何重要状态。将 aCalendar放入 aThreadLocal不太可能给您带来比您付出的更多的回报……除非分析明确显示new GregorianCalendar().

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

于 2012-12-13T23:51:27.853 回答
4

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

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

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

于 2012-12-13T05:00:16.387 回答
1

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

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

在命名空间中进行此类修改之前java.lang,只有应用程序部署者采用“一个线程池,相关应用程序的一个实例”规则才是明智的,而应用程序开发人员假设“这个线程是我的,直到 ThreadDeath 我们参与其中”。

于 2013-12-30T04:44:53.297 回答
1

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

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

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

于 2015-03-09T15:05:12.313 回答
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 接口的任务和我们的自定义 ThreadPoolExecutor,在 afterExecution 调用任务的 cleanThreadLocals();

于 2012-12-13T10:42:38.197 回答