70

有没有人有一个例子如何做到这一点?它们是否由垃圾收集器处理?我正在使用Tomcat 6。

4

7 回答 7

82

javadoc是这样说的:

“只要线程处于活动状态并且 ThreadLocal 实例可访问,每个线程都持有对其线程局部变量副本的隐式引用;在线程离开后,它的所有线程局部实例副本都将受到垃圾回收(除非存在对这些副本的其他引用)。

如果您的应用程序或(如果您正在谈论请求线程)容器使用线程池,这意味着线程不会死亡。如有必要,您需要自己处理线程局部变量。唯一干净的方法是调用该ThreadLocal.remove()方法。

您可能希望为线程池中的线程清理线程局部变量有两个原因:

  • 防止内存(或假设的资源)泄漏,或
  • 防止通过线程局部变量将信息从一个请求意外泄漏到另一个请求。

线程本地内存泄漏通常不应该是有界线程池的主要问题,因为任何线程本地都可能最终被覆盖;即当线程被重用时。ThreadLocal但是,如果您错误地一遍又一遍地创建新实例(而不是使用static变量来保存单例实例),线程局部值不会被覆盖,而是会在每个线程的threadlocals映射中累积。这可能导致严重泄漏。


假设您正在谈论在 webapp 处理 HTTP 请求期间创建/使用的线程本地,那么避免线程本地泄漏的一种方法是向ServletRequestListener您的 webapp注册 aServletContext并实现侦听器的requestDestroyed方法来清理线程本地当前线程。

请注意,在这种情况下,您还需要考虑信息从一个请求泄漏到另一个请求的可能性。

于 2010-10-06T03:02:15.003 回答
35

当您没有对实际线程局部变量的引用时,这里有一些代码可以清除当前线程中的所有线程局部变量。您还可以将其概括为清理其他线程的线程局部变量:

    private void cleanThreadLocals() {
        try {
            // Get a reference to the thread locals table of the current thread
            Thread thread = Thread.currentThread();
            Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
            threadLocalsField.setAccessible(true);
            Object threadLocalTable = threadLocalsField.get(thread);

            // Get a reference to the array holding the thread local variables inside the
            // ThreadLocalMap of the current thread
            Class threadLocalMapClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
            Field tableField = threadLocalMapClass.getDeclaredField("table");
            tableField.setAccessible(true);
            Object table = tableField.get(threadLocalTable);

            // The key to the ThreadLocalMap is a WeakReference object. The referent field of this object
            // is a reference to the actual ThreadLocal variable
            Field referentField = Reference.class.getDeclaredField("referent");
            referentField.setAccessible(true);

            for (int i=0; i < Array.getLength(table); i++) {
                // Each entry in the table array of ThreadLocalMap is an Entry object
                // representing the thread local reference and its value
                Object entry = Array.get(table, i);
                if (entry != null) {
                    // Get a reference to the thread local object and remove it from the table
                    ThreadLocal threadLocal = (ThreadLocal)referentField.get(entry);
                    threadLocal.remove();
                }
            }
        } catch(Exception e) {
            // We will tolerate an exception here and just log it
            throw new IllegalStateException(e);
        }
    }
于 2013-05-20T07:17:48.617 回答
20

没有办法清理ThreadLocal值,除非从首先将它们放入其中的线程内(或者当线程被垃圾收集时 - 不是工作线程的情况)。这意味着您应该注意在 servlet 请求完成时(或在将 AsyncContext 传输到 Servlet 3 中的另一个线程之前)清理 ThreadLocal,因为在那之后您可能永远没有机会进入该特定的工作线程,因此,在服务器未重新启动时取消部署 Web 应用程序的情况下会泄漏内存。

进行此类清理的好地方是ServletRequestListener.requestDestroyed()

如果你使用 Spring,所有必要的连接都已经就位,你可以简单地将东西放在你的请求范围内,而不用担心清理它们(这会自动发生):

RequestContextHolder.getRequestAttributes().setAttribute("myAttr", myAttr, RequestAttributes.SCOPE_REQUEST);
. . .
RequestContextHolder.getRequestAttributes().getAttribute("myAttr", RequestAttributes.SCOPE_REQUEST);
于 2012-01-25T15:07:30.807 回答
2

再次仔细阅读 Javadoc 文档:

'只要线程处于活动状态并且 ThreadLocal 实例可访问,每个线程都持有对其线程局部变量副本的隐式引用;在线程消失后,它的所有线程本地实例副本都将受到垃圾回收(除非存在对这些副本的其他引用)。'

无需清洁任何东西,泄漏存在“与”条件。因此,即使在线程存在于应用程序中的 Web 容器中,只要卸载了 webapp 类(只有在父类加载器中加载的静态类中的蜜蜂引用会阻止这种情况,这与 ThreadLocal 无关,但一般问题与具有静态数据的共享 jars )然后不再满足 AND 条件的第二条腿,因此线程本地副本有资格进行垃圾收集。

就实现符合文档而言,线程本地不能是内存泄漏的原因。

于 2017-01-01T13:38:01.900 回答
1

即使这个问题很旧,我也想贡献我的答案。我一直被同样的问题所困扰(gson threadlocal 没有从请求线程中删除),甚至可以在内存不足时重新启动服务器(这很糟糕!!)。

在设置为 dev 模式的 java web 应用程序的上下文中(因为服务器设置为在每次检测到代码更改时反弹,并且可能也在调试模式下运行),我很快了解到 threadlocals 可以很棒有时会很痛苦。我对每个请求都使用了线程本地调用。在调用内部。我有时也会使用 gson 来生成我的回复。我会将调用包装在过滤器的“try”块中,然后在“finally”块中销毁它。

我观察到的(我现在没有指标来支持这一点)是,如果我对几个文件进行了更改并且服务器在我的更改之间不断反弹,我会不耐烦并重新启动服务器(准确地说是 tomcat)从IDE。最有可能的是,我最终会遇到“内存不足”异常。

我解决这个问题的方法是在我的应用程序中包含一个 ServletRequestListener 实现,我的问题就消失了。我认为发生的事情是,在请求的中间,如果服务器会反弹几次,我的 threadlocals 没有被清除(包括 gson),所以我会收到关于 threadlocals 的警告,稍后会收到两三个警告,服务器会崩溃。随着 ServletResponseListener 显式关闭我的 threadlocals,gson 问题消失了。

我希望这是有道理的,并让您了解如何克服线程本地问题。始终在使用点附近关闭它们。在 ServletRequestListener 中,测试每个 threadlocal 包装器,如果它仍然具有对某个对象的有效引用,则在该点销毁它。

我还应该指出,将 threadlocal 作为静态变量包装在类中成为一种习惯。这样,您可以保证通过在 ServeltRequestListener 中销毁它,您不必担心同一类的其他实例挂起。

于 2013-06-05T06:45:16.963 回答
0

JVM 会自动清理 ThreadLocal 对象中的所有无引用对象。

清理这些对象的另一种方法(例如,这些对象可能是周围存在的所有线程不安全对象)是将它们放在某个 Object Holder 类中,该类基本上保存它,您可以重写 finalize 方法来清理对象驻留在其中。再次,它取决于垃圾收集器及其策略,何时调用该finalize方法。

这是一个代码示例:

public class MyObjectHolder {

    private MyObject myObject;

    public MyObjectHolder(MyObject myObj) {
        myObject = myObj;
    }

    public MyObject getMyObject() {
        return myObject;
    }

    protected void finalize() throws Throwable {
        myObject.cleanItUp();
    }
}

public class SomeOtherClass {
    static ThreadLocal<MyObjectHolder> threadLocal = new ThreadLocal<MyObjectHolder>();
    .
    .
    .
}
于 2014-04-10T03:51:22.940 回答
0

@lyaffe 的答案是 Java 6 的最佳答案。这个答案使用 Java 8 中可用的内容解决了一些问题。

@lyaffe 的答案是在MethodHandle可用之前为 Java 6 编写的。由于反射,它会遭受性能损失。如果如下使用,则MethodHandle提供对字段和方法的零开销访问。

@lyaffe 的答案也ThreadLocalMap.table很明确并且容易出现错误。现在有一种方法ThreadLocalMap.expungeStaleEntries()可以做同样的事情。

下面的代码有 3 种初始化方法来最小化调用的成本expungeStaleEntries()

private static final MethodHandle        s_getThreadLocals     = initThreadLocals();
private static final MethodHandle        s_expungeStaleEntries = initExpungeStaleEntries();
private static final ThreadLocal<Object> s_threadLocals        = ThreadLocal.withInitial(() -> getThreadLocals());

public static void expungeThreadLocalMap()
{
   Object threadLocals;

   threadLocals = s_threadLocals.get();

   try
   {
      s_expungeStaleEntries.invoke(threadLocals);
   }
   catch (Throwable e)
   {
      throw new IllegalStateException(e);
   }
}

private static Object getThreadLocals()
{
   ThreadLocal<Object> local;
   Object result;
   Thread thread;

   local = new ThreadLocal<>();

   local.set(local);   // Force ThreadLocal to initialize Thread.threadLocals

   thread = Thread.currentThread();

   try
   {
      result = s_getThreadLocals.invoke(thread);
   }
   catch (Throwable e)
   {
      throw new IllegalStateException(e);
   }

   return(result);
}

private static MethodHandle initThreadLocals()
{
   MethodHandle result;
   Field field;

   try
   {
      field = Thread.class.getDeclaredField("threadLocals");

      field.setAccessible(true);

      result = MethodHandles.
         lookup().
         unreflectGetter(field);

      result = Preconditions.verifyNotNull(result, "result is null");
   }
   catch (NoSuchFieldException | SecurityException | IllegalAccessException e)
   {
      throw new ExceptionInInitializerError(e);
   }

   return(result);
}

private static MethodHandle initExpungeStaleEntries()
{
   MethodHandle result;
   Class<?> clazz;
   Method method;
   Object threadLocals;

   threadLocals = getThreadLocals();
   clazz        = threadLocals.getClass();

   try
   {
      method = clazz.getDeclaredMethod("expungeStaleEntries");

      method.setAccessible(true);

      result = MethodHandles.
         lookup().
         unreflect(method);
   }
   catch (NoSuchMethodException | SecurityException | IllegalAccessException e)
   {
      throw new ExceptionInInitializerError(e);
   }

   return(result);
}
于 2018-11-12T15:19:11.203 回答