【问题标题】:How to clean up ThreadLocals如何清理 ThreadLocals
【发布时间】:2011-04-21 14:34:00
【问题描述】:

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

【问题讨论】:

    标签: java tomcat thread-local


    【解决方案1】:

    javadoc 是这样说的:

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

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

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

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

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


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

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

    【讨论】:

    • 感谢您的回复。问题是我只能在完成请求后删除 threadlocal。我没有简单的方法知道我什么时候完成了请求。我做的方式是我在请求的开头有一个拦截器,它设置了threadlocal(它是一个静态的)。所以我在每个请求开始时重置它...
    • 如果线程本地对象是静态的,泄漏是更易于管理的问题;即,线程池中的每个线程只泄漏(最多)一个实例(一个线程本地值)。根据线程本地值的不同,这种泄漏可能不值得担心。
    • 即使您使用静态 ThreadLocal,如果您的值引用由同一个类加载器加载的某个类,当您重新部署您的 web 应用程序时,您可能会发生类加载器泄漏。如果您使用双括号初始化可能会发生这种情况,因为这会创建一个匿名类。我为此创建了一个修复程序:github.com/codesinthedark/ImprovedThreadLocal
    【解决方案2】:

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

        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);
            }
        }
    

    【讨论】:

    • 这会删除线程的每个人的全局变量,这在 Java 6 中对我不利。java.util.concurrent.locks.ReentrantReadWriteLock 偶尔会抛出 IllegalMonitorStateException,因为我们已经删除了它的 cachedHoldCounter,所以它试图将其递减到 0 以下。这个特殊问题在 Java 8 中不会发生,但谁知道其他 ThreadLocals 对此有何反应,例如 Spring 连接或 log4j MDC。
    • 我们是否需要检查这个 webapp 的类加载器是否加载了 threadLocal 值? threadLocal.get().getClass().getClassLoader() == Thread.currentThread().getContextClassLoader()
    【解决方案3】:

    没有办法清理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);
    

    【讨论】:

      【解决方案4】:

      再次仔细阅读 Javadoc 文档:

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

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

      线程本地不可能是内存泄漏的原因,只要实现符合文档。

      【讨论】:

      • 以及链接的实际链接参考:docs.oracle.com/javase/7/docs/api/java/lang/…)。一般来说,你有很少的容器请求处理线程,每个线程都有一个本地线程持有一个computationally expensive to create item(可以根据请求更新)并且仅在该请求/线程中引用/使用。
      【解决方案5】:

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

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

      我观察到的(我目前没有指标可以支持)是,如果我对多个文件进行了更改并且服务器在我的更改之间不断跳动,我会不耐烦并重新启动服务器(tomcat 到准确)来自IDE。最有可能的是,我最终会遇到“内存不足”异常。

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

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

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

      【讨论】:

        【解决方案6】:

        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>();
            .
            .
            .
        }
        

        【讨论】:

          【解决方案7】:

          @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);
          }
          

          【讨论】:

          • MethodHandle 速度很快,但不是零开销。在某些情况下,它是even slower than reflection。也不是even supported by Graal VM static-image,因为只要可以静态确定类和方法名称(即常量),反射就是如此......所以在方法句柄的当前状态下@lyaffe 的答案可能更好。
          • MethodHandles 在“甚至比反射还慢”的网页中使用的第一种方式肯定很慢。但是,如果您将 MethodHandle 放在 static final 字段中(网页中的第二种方式和我的回答中的方式),那么 JIT 消除了开销。文章说这是没用的;但是,我在代码的很多地方都使用了static final MethodHandles。我觉得它很有用。我猜有用性取决于用例。
          • 嗯,对我/我们来说更大的问题是我们希望最终将 Graal VM 用于我们的微服务。对于这些服务,我们不需要上述特定代码,但不支持 MethodHandle 的 Graal VM 确实让我停下来。由于您为 Oracle 工作(是的,我点击了您的个人资料 :))也许您知道他们是否计划支持他们(方法句柄)?
          • 很遗憾,我无法回答 Graal VM 是否支持MethodHandle。我什至不知道 Graal VM 团队的成员是谁。
          猜你喜欢
          • 2020-01-05
          • 2019-05-11
          • 2011-06-30
          • 1970-01-01
          • 1970-01-01
          • 2010-12-27
          • 2020-04-16
          • 2017-01-02
          相关资源
          最近更新 更多