简体   繁体   English

Java ThreadPool中的ThreadLocal值是否被GC?

[英]Whether ThreadLocal value is GCed in Java ThreadPool?

In my code, I want to test ThreadLocal's GC strategy. 在我的代码中,我想测试ThreadLocal的GC策略。 I use two methods. 我使用两种方法。 One is ThreadPool , the other is a self-created thread. 一个是ThreadPool ,另一个是自创建的线程。 In the first scenarios, JVM doesn't GC Thread's ThreadLocalMap seemly(No finalize() output). 在第一种情况下, JVM似乎没有GC Thread的ThreadLocalMap (没有finalize()输出)。 The other works well. 另一个效果很好。

I've found. 我找到了 In October 2007, Josh Bloch (co-author of java.lang.ThreadLocal along with Doug Lea) wrote: 2007年10月,Josh Bloch(java.lang.ThreadLocal和Doug Lea的合著者)写道:

"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." “使用线程池需要格外小心。草率使用线程池与草率使用线程本地变量相结合会导致意外的对象保留,这在许多地方都已提到。”

I guess ThreadPool may be dangerous to use ThreadLocal . 我猜想ThreadPool使用ThreadLocal可能很危险。

Here is my code(JDK8 environment) 这是我的代码(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");
    }

}

After running the first main() function. 运行第一个main()函数之后。 None of the SimpleDateFormat object is GCed. 没有任何SimpleDateFormat对象被GC。 The second main() function indeed does that job. 第二个main()函数确实可以完成这项工作。

first main function 第一main功能 第一主要功能

second main function 第二main功能 第二主要功能

Edit #1 Thanks to Gray's remind. 编辑#1感谢Gray的提醒。 The real problem, which results in no output in the function finalize() , is the ThreadPool may not be truly collected. 真正的问题是,可能未真正收集ThreadPool ,导致函数finalize()中没有输出。 In the test code, only shutdown() was used. 在测试代​​码中,仅使用shutdown() However, the worker threads may not be collected after this process. 但是,此过程之后可能无法收集工作线程。 So more safer way is invoking awaitTermination() . 因此,更安全的方法是调用awaitTermination() This function does generate all worker threads instance, and the resource those belongs to is collected, spefically ThreadLocalMap . 此函数确实生成所有工作线程实例,并且收集属于它们的资源,特别是ThreadLocalMap

Here is the revision of the main() with ThreadPool 这是ThreadPoolmain()的修改

// 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();

    }

This version of main() works well, all the collection message from finalize() method are printed. 此版本的main()效果很好,所有来自finalize()方法的收集消息都将被打印出来。

Finally, Java GC may not collect the value when the instance of Entry 's key has no stable reference. 最后,当Entry的键的实例没有稳定的引用时,Java GC可能不会收集该 As ThreadLocalMap 's key is the weak reference, the Entry 's key becomes null . 由于ThreadLocalMap的键是弱引用,因此Entry的键变为null However, the Entry 's value is not GCed. 但是, Entry的值不被GCed。 This conclusion may be proved in my test. 这个结论可以在我的测试中得到证明。

Instances of ThreadLocal itself are merely a view into a map stored on the thread itself. ThreadLocal本身的实例仅仅是存储在线程本身上的映射视图。 The instance being collected does not actually guarantee that the reference is severed. 所收集的实例实际上并不能保证参考被切断。

It can be approximated as threadInstance.privateField = WeakHashMap<ThreadLocal<T>,T> . 可以近似为threadInstance.privateField = WeakHashMap<ThreadLocal<T>,T>

That means if the Thread instance becomes unreachable so become all associated values held by ThreadLocal . 这意味着如果Thread实例变得不可访问,那么将成为ThreadLocal持有的所有关联值。 On the other hand when the ThreadLocal instance becomes unreachable that only means the map key is nulled (being a weak reference), the value is still held alive by the map until some accesses to the map clean the value.The map cleaning is performed lazily, so cleaning up ThreadLocal references does not have the same effect as letting threads terminate. 另一方面,当ThreadLocal实例变得不可访问时,这仅意味着映射键为空(作为弱引用),该值仍保持为活动状态,直到对地图的某些访问清除了该值为止。 ,因此清理ThreadLocal引用与让线程终止不会产生相同的效果。 The third way of cleaning it is calling threadLocal.remove() from within the thread. 清除它的第三种方法是从线程内部调用threadLocal.remove()

And of course it's a common pattern to have shared static final ThreadLocal<T> tl accessors within a class. 当然,在类中共享static final ThreadLocal<T> tl访问器是一种常见的模式。 When combined with a thread pool that means those values will stay alive as long as the thread pool does unless you use remove() 当与线程池结合使用时,这意味着这些值将与线程池一样有效,除非您使用remove()

I guess ThreadPool may be dangerous to use ThreadLocal. 我猜想ThreadPool使用ThreadLocal可能很危险。

I wouldn't go this far. 我不会走那么远。 I would say that you need to take into account that the ThreadLocal storage won't be reaped unless the thread itself terminates. 我要说的是,您需要考虑到除非线程本身终止,否则不会收获ThreadLocal存储。

But in looking at your test code, there are a lot of problems with both the ExecutorService and direct thread main methods. 但是在查看测试代码时, ExecutorService和直接线程主方法都存在很多问题。 In both cases you are not properly joining with the completed threads. 在这两种情况下,您都无法正确地使用已完成的线程。 Ditch the CountDownLatch and do the following before the gc() call: 放弃CountDownLatch并在gc()调用之前执行以下操作:

for (int i = 0; i < 10; i++) {
    all[i].join();
}

or 要么

es.shutdown();
es.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);

But the real problem with your code is that you have a race condition with the Finalizer thread. 但是代码的真正问题在于,终结器线程具有竞争条件。 The gc thread finishes but the actually finalizing of the objects happens in another "Finalizer" thread after the GC completed. gc线程完成了,但是对象的最终确定发生在GC完成之后的另一个“ Finalizer”线程中。 If you just put a 1 second sleep at the end of the main() you should see all 10 SDFs reaped. 如果仅在main()的末尾放置1秒钟的睡眠,您应该会看到所有10个SDF都已收获。

What this demonstrates really is that it is hard to force objects to the GC'd in such a way. 这实际上表明,很难以这种方式将对象强制发送到GC。 Putting System.out.println(...) commands in a finalizer() gives me the chills even thinking about it even though I know you are doing it to learn more about ThreadLocal 's memory usage. System.out.println(...)命令放在finalizer() ,即使我知道要了解更多有关ThreadLocal的内存使用情况,也正在思考这个问题。

I think that storing things in ThreadLocal s if done carefully shouldn't be a problem. 我认为,如果仔细完成,将事情存储在ThreadLocal应该不是问题。 In your thread's run method, I would just do a try / finally block and make sure to do a threadLocal.remove() in the finally so the thread cleans itself up before exiting. 在您线程的run方法中,我只需要执行try / finally块,并确保在finally执行一个threadLocal.remove() ,以便线程在退出之前清理自身。 But I don't even bother with that if I have a background thread which is running for the life of my application. 但是,如果我有一个在我的应用程序生命周期中一直运行的后台线程,我什至不理会。 It is really only threads that come and go that you need to be particularly worried about. 实际上,只有特别多的线程需要您特别担心。

Lastly, there is no need for a ThreadLocal field to be volatile and it should be a static within the ParseDate if possible. 最后,不需要ThreadLocal字段是volatile并且如果可能的话,它在ParseDate应该是static的。

Hope this helps. 希望这可以帮助。

A Thread in a ThreadPool may never terminate until the ThreadPool does. ThreadPoolThread可能永远不会终止,直到ThreadPool终止。 That's the whole point of ThreadPool . 这就是ThreadPool So it never gets GC'd. 因此,它永远不会得到GC。 So of course the Thread 's ThreadLocal doesn't get GC'd either. 因此,当然, ThreadThreadLocal也不会得到GC。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM