【发布时间】:2011-04-21 14:34:00
【问题描述】:
有没有人举例说明如何做到这一点?它们是否由垃圾收集器处理?我正在使用 Tomcat 6。
【问题讨论】:
标签: java tomcat thread-local
有没有人举例说明如何做到这一点?它们是否由垃圾收集器处理?我正在使用 Tomcat 6。
【问题讨论】:
标签: java tomcat thread-local
javadoc 是这样说的:
"只要线程处于活动状态且 ThreadLocal 实例可访问,每个线程都持有对其线程局部变量副本的隐式引用;在线程离开后,它的所有线程局部实例副本均受制于垃圾回收(除非存在对这些副本的其他引用)。
如果您的应用程序或(如果您正在谈论请求线程)容器使用线程池,这意味着线程不会死亡。如有必要,您需要自己处理线程局部变量。唯一干净的方法是调用ThreadLocal.remove() 方法。
您可能希望为线程池中的线程清理线程局部变量的原因有两个:
线程本地内存泄漏通常不应该是有界线程池的主要问题,因为任何线程本地都可能最终被覆盖;即当线程被重用时。但是,如果您错误地一遍又一遍地创建新的ThreadLocal 实例(而不是使用static 变量来保存单例实例),线程本地值不会被覆盖,并且会在每个实例中累积线程的threadlocals 映射。这可能会导致严重的泄漏。
假设您谈论的是在 webapp 处理 HTTP 请求期间创建/使用的线程本地,那么避免线程本地泄漏的一种方法是使用 webapp 的 ServletContext 注册 ServletRequestListener 并实现监听器的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);
}
}
【讨论】:
java.util.concurrent.locks.ReentrantReadWriteLock 偶尔会抛出 IllegalMonitorStateException,因为我们已经删除了它的 cachedHoldCounter,所以它试图将其递减到 0 以下。这个特殊问题在 Java 8 中不会发生,但谁知道其他 ThreadLocals 对此有何反应,例如 Spring 连接或 log4j MDC。
没有办法清理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条件不再满足,因此线程本地副本有资格进行垃圾回收。
线程本地不可能是内存泄漏的原因,只要实现符合文档。
【讨论】:
computationally expensive to create item(可以根据请求更新)并且仅在该请求/线程中引用/使用。
我想贡献我对这个问题的回答,即使它已经过时了。我一直被同样的问题所困扰(gson threadlocal 没有从请求线程中删除),甚至可以在内存不足时重新启动服务器(这很糟糕!!)。
在设置为 dev 模式的 java web 应用程序的上下文中(服务器设置为在每次检测到代码更改时反弹,并且可能还以调试模式运行),我很快了解到 threadlocals可能很棒,有时会很痛苦。我对每个请求都使用了线程本地调用。在调用内部。我有时也会使用 gson 来生成我的回复。我会将 Invocation 包装在过滤器的“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);
}
【讨论】:
MethodHandles 在“甚至比反射还慢”的网页中使用的第一种方式肯定很慢。但是,如果您将 MethodHandle 放在 static final 字段中(网页中的第二种方式和我的回答中的方式),那么 JIT 消除了开销。文章说这是没用的;但是,我在代码的很多地方都使用了static final MethodHandles。我觉得它很有用。我猜有用性取决于用例。
MethodHandle。我什至不知道 Graal VM 团队的成员是谁。