是否在ThreadPool中对ThreadLocal值进行了GC?

时间:2017-03-26 11:55:04

标签: java multithreading garbage-collection threadpoolexecutor thread-local

在我的代码中,我想测试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()函数确实可以完成这项工作。

第一个main功能 first main function

第二个main功能 second main function

编辑#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。这个结论可以在我的测试中证明。

3 个答案:

答案 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。