在我的代码中,我想测试ThreadLocal的GC策略。我用两种方法。一个是ThreadPool
,另一个是自创的线程。在第一种情况下,JVM
似乎没有GC线程ThreadLocalMap
(No finalize()
输出)。另一个效果很好。
我找到了。 2007年10月,Josh Bloch(java.lang.ThreadLocal和Doug Lea的合着者)写道:
“线程池的使用需要极其谨慎。使用线程 池与本地线程的粗心使用相结合可能会导致 意外的物体保留,正如许多地方所指出的那样。“
我猜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
对象都没有被GCed。第二个main()
函数确实可以完成这项工作。
编辑#1
感谢Gray的提醒。导致函数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
的值不是GCed。这个结论可以在我的测试中证明。
答案 0 :(得分:2)
var selectedItem = dgdProcessList.SelectedItem as Item;
if(selectedItem != null)
MessageBox.Show(selectedItem.ID.ToString());
本身的实例仅仅是存储在线程本身上的地图的视图。收集的实例实际上并不能保证引用被切断。
它可以近似为ThreadLocal
。
这意味着如果threadInstance.privateField = WeakHashMap<ThreadLocal<T>,T>
实例无法访问,则成为Thread
所持有的所有关联值。另一方面,当ThreadLocal
实例变得无法访问时,这意味着地图键被置为空(作为弱引用),地图仍然保持该值,直到某些访问地图清除该值。清理工作是懒惰的,因此清理ThreadLocal
引用与让线程终止的效果不同。
清理它的第三种方法是从线程中调用ThreadLocal
。
当然,在类中共享threadLocal.remove()
个访问器是一种常见的模式。与线程池结合使用时,只要线程池执行,这些值将保持活动状态,除非您使用static final ThreadLocal<T> tl
答案 1 :(得分:1)
我猜ThreadPool使用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线程完成但实际完成的对象发生在另一个&#34; Finalizer&#34; GC完成后的线程。如果您只是在main()
的末尾进行1秒钟的睡眠,您应该会看到所有10个SDF都被收获。
这表明真的很难以这种方式强迫对象加入GC。将System.out.println(...)
命令放在finalizer()
中,即使我知道您正在这样做以了解有关ThreadLocal
内存使用情况的更多信息,也会让我感到沮丧。
我认为如果小心谨慎地将事物存储在ThreadLocal
中就不应该成为问题。在你的线程运行方法中,我只会执行try / finally
块并确保在threadLocal.remove()
中执行finally
,以便线程在退出之前自行清理。但是如果我有一个后台线程在我的应用程序的生命周期中运行,我甚至不会为此烦恼。实际上只有线程来来去去才需要特别担心。
最后,ThreadLocal
字段不需要volatile
,如果可能,static
内的字段应为ParseDate
。
希望这有帮助。
答案 2 :(得分:0)
Thread
中的ThreadPool
可能永远不会终止,直到ThreadPool
为止。这就是ThreadPool
的重点。所以它永远不会得到GC。当然,Thread
的{{1}}也不会得到GC。