有没有人有一个例子如何做到这一点?它们是否由垃圾收集器处理?我正在使用Tomcat 6。
7 回答
javadoc是这样说的:
“只要线程处于活动状态并且 ThreadLocal 实例可访问,每个线程都持有对其线程局部变量副本的隐式引用;在线程离开后,它的所有线程局部实例副本都将受到垃圾回收(除非存在对这些副本的其他引用)。
如果您的应用程序或(如果您正在谈论请求线程)容器使用线程池,这意味着线程不会死亡。如有必要,您需要自己处理线程局部变量。唯一干净的方法是调用该ThreadLocal.remove()
方法。
您可能希望为线程池中的线程清理线程局部变量有两个原因:
- 防止内存(或假设的资源)泄漏,或
- 防止通过线程局部变量将信息从一个请求意外泄漏到另一个请求。
线程本地内存泄漏通常不应该是有界线程池的主要问题,因为任何线程本地都可能最终被覆盖;即当线程被重用时。ThreadLocal
但是,如果您错误地一遍又一遍地创建新实例(而不是使用static
变量来保存单例实例),线程局部值不会被覆盖,而是会在每个线程的threadlocals
映射中累积。这可能导致严重泄漏。
假设您正在谈论在 webapp 处理 HTTP 请求期间创建/使用的线程本地,那么避免线程本地泄漏的一种方法是向ServletRequestListener
您的 webapp注册 aServletContext
并实现侦听器的requestDestroyed
方法来清理线程本地当前线程。
请注意,在这种情况下,您还需要考虑信息从一个请求泄漏到另一个请求的可能性。
当您没有对实际线程局部变量的引用时,这里有一些代码可以清除当前线程中的所有线程局部变量。您还可以将其概括为清理其他线程的线程局部变量:
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);
}
}
没有办法清理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);
再次仔细阅读 Javadoc 文档:
'只要线程处于活动状态并且 ThreadLocal 实例可访问,每个线程都持有对其线程局部变量副本的隐式引用;在线程消失后,它的所有线程本地实例副本都将受到垃圾回收(除非存在对这些副本的其他引用)。'
无需清洁任何东西,泄漏存在“与”条件。因此,即使在线程存在于应用程序中的 Web 容器中,只要卸载了 webapp 类(只有在父类加载器中加载的静态类中的蜜蜂引用会阻止这种情况,这与 ThreadLocal 无关,但一般问题与具有静态数据的共享 jars )然后不再满足 AND 条件的第二条腿,因此线程本地副本有资格进行垃圾收集。
就实现符合文档而言,线程本地不能是内存泄漏的原因。
即使这个问题很旧,我也想贡献我的答案。我一直被同样的问题所困扰(gson threadlocal 没有从请求线程中删除),甚至可以在内存不足时重新启动服务器(这很糟糕!!)。
在设置为 dev 模式的 java web 应用程序的上下文中(因为服务器设置为在每次检测到代码更改时反弹,并且可能也在调试模式下运行),我很快了解到 threadlocals 可以很棒有时会很痛苦。我对每个请求都使用了线程本地调用。在调用内部。我有时也会使用 gson 来生成我的回复。我会将调用包装在过滤器的“try”块中,然后在“finally”块中销毁它。
我观察到的(我现在没有指标来支持这一点)是,如果我对几个文件进行了更改并且服务器在我的更改之间不断反弹,我会不耐烦并重新启动服务器(准确地说是 tomcat)从IDE。最有可能的是,我最终会遇到“内存不足”异常。
我解决这个问题的方法是在我的应用程序中包含一个 ServletRequestListener 实现,我的问题就消失了。我认为发生的事情是,在请求的中间,如果服务器会反弹几次,我的 threadlocals 没有被清除(包括 gson),所以我会收到关于 threadlocals 的警告,稍后会收到两三个警告,服务器会崩溃。随着 ServletResponseListener 显式关闭我的 threadlocals,gson 问题消失了。
我希望这是有道理的,并让您了解如何克服线程本地问题。始终在使用点附近关闭它们。在 ServletRequestListener 中,测试每个 threadlocal 包装器,如果它仍然具有对某个对象的有效引用,则在该点销毁它。
我还应该指出,将 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>();
.
.
.
}
@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);
}