如何清除 ThreadLocals

有人能举个例子吗?它们是由垃圾收集器处理的吗?我用的是 Tomcat 6。

84997 次浏览

Javadoc 说:

“只要线程是活的,ThreadLocal 实例是可访问的,每个线程都保存着对其线程本地变量副本的隐式引用; 在一个线程消失后,它所有的线程本地实例副本都会受到垃圾收集的影响(除非存在对这些副本的其他引用)。

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

有两个原因可能需要清理线程池中线程的线程局部变量:

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

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


假设你正在讨论的线程局部变量是在 Web 应用处理 HTTP 请求的过程中创建/使用的,那么避免线程局部泄漏的一个方法是在你的 Web 应用的 ServletContext中注册一个 ServletRequestListener,然后实现侦听器的 requestDestroyed方法来清理当前线程的线程局部变量。

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

除了从 把他们放进去的那根线内部(或者当线程被垃圾收集时——工作线程不是这种情况)之外,没有其他方法可以清除 ThreadLocal值。这意味着当一个 Servlet 请求完成时(或者在将 AsyncContext 传输到 Servlet 3中的另一个线程之前) ,你应该注意清理 ThreadLocal 的内存,因为在那之后你可能永远没有机会进入那个特定的工作线程,因此,当你的 web 应用没有部署而服务器没有重新启动时,会泄漏内存。

进行这种清理的一个好地方是 Request销毁()

如果您使用 Spring,所有必要的连接都已经就位,您可以简单地将内容放入请求作用域,而不用担心清理它们(这是自动发生的) :

RequestContextHolder.getRequestAttributes().setAttribute("myAttr", myAttr, RequestAttributes.SCOPE_REQUEST);
. . .
RequestContextHolder.getRequestAttributes().getAttribute("myAttr", RequestAttributes.SCOPE_REQUEST);

下面是一些代码,当您没有对实际线程局部变量的引用时,它们可以清除当前线程中的所有线程局部变量。还可以将其泛化为清除其他线程的线程本地变量:

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

尽管这个问题已经很老了,但我还是想把我的答案贡献出来。我曾经被同样的问题所困扰(gson threadlocal 没有从请求线程中删除) ,甚至在服务器内存不足时重新启动服务器(这太糟糕了!).

在一个设置为 dev 模式的 java web 应用的上下文中(服务器每次感觉到代码的变化都会弹出,可能还会在调试模式下运行) ,我很快了解到 threadlocal 可以很棒,有时候也会很痛苦。我对每个请求都使用一个 threadlocal 调用。祈祷内容。我有时也会用 gson 来产生我的反应。我将在过滤器中的“ try”块中包装调用,并在“ finally”块中销毁它。

我所观察到的(目前我还没有指标来支持这一点)是,如果我对几个文件进行了更改,并且服务器在我的更改之间不断跳转,我就会失去耐心,从 IDE 重新启动服务器(准确地说是 tomcat)。最有可能的情况是,我最终会得到一个“内存不足”异常。

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

我希望这是有意义的,并给您一个如何克服线程本地问题的想法。总是在使用点附近关闭它们。在 ServletRequestListener 中,测试每个线程本地包装器,如果它仍然有对某个对象的有效引用,则在该点销毁它。

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

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

仔细阅读 Javadoc 文档:

只要线程是活的,ThreadLocal 实例是可访问的,每个线程都有一个对其线程本地变量副本的隐式引用; 在一个线程消失后,它所有的线程本地实例副本都会受到垃圾收集的影响(除非存在对这些副本的其他引用)。 '

没有必要清理任何东西,有一个’与’条件泄漏生存。所以即使在线程存活到应用程序的 Web 容器中, 只要 webapp 类被卸载(只有在父类加载器中加载静态类中的引用才会阻止这种情况,这与 ThreadLocal 无关,而是与静态数据共享 jar 的普遍问题) ,那么 AND 条件的第二部分就不再满足,因此线程本地副本有资格进行垃圾收集。

线程本地不会导致内存泄漏,只要实现符合文档要求。

@ lyaffe 的答案是 Java 6的最佳选择。这个答案使用 Java8中提供的内容解决了一些问题。

@ lyaffe 的答案是在 MethodHandle可用之前为 Java6编写的。由于反思,它受到了绩效惩罚。如果按以下方式使用,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);
}
final ThreadLocal<T> old = backend;


// try to clean by reflect
try {
// BGN copy from apache ThreadUtils#getAllThreads
ThreadGroup systemGroup = Thread.currentThread().getThreadGroup();
while (systemGroup.getParent() != null) {
systemGroup = systemGroup.getParent();
}
int count = systemGroup.activeCount();
Thread[] threads;
do {
threads = new Thread[count + (count / 2) + 1]; //slightly grow the array size
count = systemGroup.enumerate(threads, true);
//return value of enumerate() must be strictly less than the array size according to javadoc
} while (count >= threads.length);
// END


// remove by reflect
final Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
threadLocalsField.setAccessible(true);


Class<?> threadLocalMapClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
Method removeMethod = threadLocalMapClass.getDeclaredMethod("remove", ThreadLocal.class);
removeMethod.setAccessible(true);


for (int i = 0; i < count; i++) {
final Object threadLocalMap = threadLocalsField.get(threads[i]);
if (threadLocalMap != null) {
removeMethod.invoke(threadLocalMap, old);
}
}


}
catch (Exception e) {
throw new ThreadLocalAttention(e);
}