java - Java ThreadPool中ThreadLocal值是否被GC?

标签 java multithreading garbage-collection threadpoolexecutor thread-local

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

第一个main函数 first main function

第二个main函数 second main function

编辑#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/

相关文章:

java - 无法通过maven脚本部署ear应用程序

java - java中的handlebars.js实现是否存在?

multithreading - 亲子过程

java - 堆增长和下降

java - 我们是否可以手动触发 Full GC,或者使用堆的某个百分比的参数来触发 Full GC,这是建议的吗?

java - 在 ReentrantReadWriteLock 中是否有 WriteLock 优先于 ReadLock

java - 另一个类中的 SQL 更新

c# - 为重负载任务设置低线程优先级

java - Java多线程网络爬虫中控制线程数量和对象访问

javascript - jQuery 内存泄漏模式和原因