在我的代码中,我想测试ThreadLocal的GC策略。我使用两种方法。一种是ThreadPool
,另一种是自创建的线程。在第一种情况下,JVM
似乎不会 GC Thread 的 ThreadLocalMap
(没有 finalize()
输出)。另一个效果很好。
我发现了。 2007 年 10 月,Josh Bloch(与 Doug Lea 共同开发 java.lang.ThreadLocal)写道:
"The use of thread pools demands extreme care. Sloppy use of thread pools in combination with sloppy use of thread locals can cause unintended object retention, as has been noted in many places."
我猜 ThreadPool
使用 ThreadLocal
可能很危险。
这是我的代码(JDK8环境)
public class ThreadLocalDemo_Gc {
static volatile ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>(){
// overwrite finalize, such that the message will be printed when GC happens.
protected void finalize() throws Throwable{
System.out.println(this.toString() + " is gc(threadlocal)");
}
};
// Let the main thread wait for all workers.
static volatile CountDownLatch cd = new CountDownLatch(10);
public static class ParseDate implements Runnable{
int i = 0;
public ParseDate(int i) {
super();
this.i = i;
}
@Override
public void run() {
try {
if(tl.get() == null){
tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"){
// overwrite finalize, such that the message will be printed when GC happens.
protected void finalize() throws Throwable {
System.out.println(this.toString() + " is gc(sdf)");
}
});
// new sdf object is created in ThreadLocalMap
System.out.println(Thread.currentThread().getId() + ":create SimpleDateFormat");
}
Date t = tl.get().parse("2017-3-26 17:03:" + i % 60);
} catch (ParseException e) {
e.printStackTrace();
} finally {
cd.countDown();
}
}
}
// code with ThreadPool
// public static void main(String[] args) throws InterruptedException {
// ExecutorService es = Executors.newFixedThreadPool(10);
//
// for(int i = 0; i < 10; i++){
// es.execute(new ParseDate(i));
// }
// cd.await();
//
// System.out.println("mission complete");
//
// tl = null; // free the weak reference
// System.gc();
// System.out.println("First GC complete");
// es.shutdown();
// }
// not pooling threads
public static void main(String[] args) throws InterruptedException {
Thread[] all = new Thread[10];
for(int i = 0; i < 10; i++){
all[i] = new Thread(new ParseDate(i));
}
for(int i = 0; i < 10; i++){
all[i].start();
}
cd.await();
tl = null;
System.gc();
System.out.println("First GC complete");
}
}
运行第一个 main()
函数后。所有 SimpleDateFormat
对象都未被 GC 处理。第二个 main()
函数确实完成了这项工作。
编辑#1
感谢格雷的提醒。导致finalize()
函数没有输出的真正问题是ThreadPool
可能没有被真正收集。在测试代码中,仅使用了shutdown()
。但是,在此过程之后可能不会收集工作线程。因此更安全的方法是调用 awaitTermination()
。该函数确实生成所有工作线程实例,并收集这些线程所属的资源,特别是ThreadLocalMap
。
这里是 main()
与 ThreadPool
的修订版
// code with ThreadPool
public static void main(String[] args) throws InterruptedException {
ExecutorService es = Executors.newFixedThreadPool(10);
for(int i = 0; i < 10; i++){
es.execute(new ParseDate(i));
}
cd.await();
es.shutdown();
es.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
System.gc();
}
此版本的 main()
运行良好,打印了来自 finalize()
方法的所有集合消息。
最后,当Entry
的键的实例没有稳定的引用时,Java GC可能不会收集值。由于ThreadLocalMap
的键是弱引用,因此Entry
的键变为null
。但是,Entry
的值不会被GC。这个结论或许可以在我的测试中得到证实。
最佳答案
I guess ThreadPool may be dangerous to use ThreadLocal.
我不会走这么远。我想说你需要考虑到 ThreadLocal
除非线程本身终止,否则不会回收存储。
但是在查看您的测试代码时,ExecutorService
都存在很多问题。和直接线程主方法。在这两种情况下,您都没有正确连接已完成的线程。抛弃CountDownLatch
并在 gc()
之前执行以下操作调用:
for (int i = 0; i < 10; i++) {
all[i].join();
}
或
es.shutdown();
es.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
但是代码的真正问题是 Finalizer 线程存在竞争条件。 gc 线程完成,但对象的实际终结发生在 GC 完成后的另一个“Finalizer”线程中。如果你只是在 main()
的末尾放置 1 秒 sleep 您应该会看到所有 10 个 SDF 均已收获。
这真正说明的是,以这种方式强制对象进行 GC 是很困难的。推杆System.out.println(...)
finalizer()
中的命令即使我知道你这样做是为了了解更多关于 ThreadLocal
的信息,但一想到它就让我感到不寒而栗。的内存使用情况。
我认为将东西存储在ThreadLocal
中如果仔细做应该不会有问题。在你线程的 run 方法中,我只需执行 try / finally
阻止并确保执行 threadLocal.remove()
在 finally
因此线程在退出之前会自行清理。但如果我有一个在应用程序的生命周期中运行的后台线程,我什至不会为此烦恼。实际上,您需要特别担心的只是来来去去的线程。
最后,不需要 ThreadLocal
字段为volatile
它应该是 static
ParseDate
内如果可能的话。
希望这有帮助。
关于java - Java ThreadPool中ThreadLocal值是否被GC?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43028388/