5

I have created a thread which in turn creates a ThreadPoolExecutor and submits some long running tasks to it. At some point, the original thread dies due to unhandled exception/error. What should happen to the executor (it's local to that dead thread, no external references to it)? Should it be GCed or not?

EDIT: this question was formulated incorrectly from the beginning, but I will leave it as Gray provided some good details of how TPE work.

vlaku
  • 1,534
  • 2
  • 14
  • 27
  • 5
    Any object that has lost all references to it is eligible for garbage collection. You're overthinking it. – Michael Feb 22 '19 at 11:08
  • 1
    True, but that not exactly what I was asking. Question was related to spawned threads, sorry I wasn't clear enough. – vlaku Feb 22 '19 at 11:52
  • Your "EDIT:" should be removed @vlaku. I don't think it is correct. See my answer. – Gray Feb 22 '19 at 16:13
  • In`ThreadPoolExecutor` all exceptions thrown by your `Runnable` are captured. The `Thread` never dies until the pool wants it to. If you have a pool with a fixed size, no threads will ever terminate due to your code unless the pool is shutdown. You can go and look at all the code in `ThreadPoolExeucotr` for yourself. – Johnny V Feb 23 '19 at 23:19
  • Remember to accept an answer if it was helpful. – Gray Nov 14 '20 at 17:24

2 Answers2

2

Threads are so called GC roots. This means among other things, that a running (or an unstarted) thread can't be collected. It also means that objects that are being referenced from those threads can't be collected, which is why you can do things like new Thread(new MyRunnable()).start(), or have threadpools running without you having any reference to them.

If the thread is a daemon, it can stop automatically if all other non-daemon threads have stopped. You can have threadpools with daemon threads, but the best thing is to make sure that things are cleaned up properly (i.e. an exception shouldn't kill the thread that's supposed to eventually stop and cleanup the threadpool).

Gray
  • 115,027
  • 24
  • 293
  • 354
  • 1
    It's because it initially contained some insulting language that wasn't appropriate @GhostCat. Check out the edit history. – Gray Feb 23 '19 at 15:09
  • Yes they are GC Roots but you're confusing why and when that is happening. Thread GC Roots contain your call stack objects, this prevents Objects from being collected when passed into a function. The `new Runnable()` inside of `new Thread(new Runnable())` cannot be GC'd because the `Runnable` is reference variable in the `Thread` class called `target`. The `Thread` and objects in the call stack cannot be collected until the call stack releases them or the Thread terminates. If the Thread is not running and is not referenced, it is available for collection at any time. – Johnny V Feb 23 '19 at 23:13
  • Daemon threads is not related to this question. Daemon threads only affect whether or not the JVM will terminate. – Gray Feb 26 '19 at 19:08
2

What should happen to executor (it's local to that dead thread, no external references to it)? Should it be GCed or not?

The answer is more complex than "yes, it will be if no references are to it". It depends on whether or not the threads running in the ThreadPoolExecutor are still running. This in turn depends on what type of TPE was created and whether or not the "long running tasks" that were submitted to it have finished.

For example, if the tasks have not finished then the threads will still be running. Even if they have finish, if you had a TPE with core threads that did not set allowCoreThreadTimeOut(true) then the threads will not stop. The JVM never garbage-collects a running thread since they are considered GC "roots":

... running threads are, by definition, immune to GC. The GC begins its work by scanning "roots", which are deemed always reachable; roots include global variables ("static fields" in Java-talk) and the stacks of all running threads ...

So the next question is if the threads have references back to the ThreadPoolExecutor and I believe they do. The Worker inner class is the Runnable that is stored in thread.target and is being executed by the Thread so it can't be GC'd. Worker is not static so it has implied references to the outer ThreadPoolExecutor instance. The run() method is actually calling the ThreadPoolExecutor.runWorker() method which references all of the task queues managed by the ThreadPoolExecutor. So the running threads hold references back to the Worker and the TPE so the garbage-collector can't collect the TPE.

For example, here's a typical stack frame of a running pool thread which references the TPE:

java.lang.Thread.sleep(Native Method)
com.j256.GcTester$1.run(GcTesteri.java:15)
java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
java.util.concurrent.FutureTask.run(FutureTask.java:266)
>> java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
>> java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
java.lang.Thread.run(Thread.java:748)

If, however, the thread-pool tasks have all finished and it has 0 core-threads or the core-threads have timed out, then there would be no Worker threads associated with the ThreadPoolExecutor. Then the TPE would be garbage collected because there were no references to it aside from cyclic ones that the GC is smart enough to detect.

Here's a little sample test program which demonstrates it. If there is 1 core thread then the TPE will never be shut down (through finalize()) even after the worker thread exits after noticing that /tmp/x file exists. This is true even though the main thread doesn't have a reference to it. If, however, there are 0 core threads then after the thread times out (here after 1 second after finishing the last task) the TPE will be collected.

public class GcTester {
    public static void main(String[] args) {
        int numCore = 1; // set to 0 to have it GC'd once /tmp/x file exists
        ExecutorService pool =
                new ThreadPoolExecutor(numCore, Integer.MAX_VALUE,
                        1, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()) {
                    protected void terminated() {
                        System.out.println(this + " terminated");
                    }
                };
        pool.submit(new Runnable() {
            public void run() {
                while (true) {
                    Thread.sleep(100); // need to handle exception here
                    if (new File("/tmp/x").exists()) {
                        System.out.println("thread exiting");
                        return;
                    }
                }
            }
        });
        pool = null; // allows it to be gc-able
        while (true) {
            Thread.sleep(1000);  // need to handle exception here
            System.gc();         // pull the big GC handle
        }
    }
}
Gray
  • 115,027
  • 24
  • 293
  • 354
  • Yes, I suspected you'd say that. Workers are not exposed (AFAIK) so nothing can hold a reference to a Worker except the executor itself. This cyclic reference is irrelevant to the GC and both are still eligible for removal as soon as the reference to the executor is lost. – Michael Feb 22 '19 at 15:21
  • But not if the `Thread` is running @Michael. If the thread is running then it a GC "root". That root has references to the TPE. – Gray Feb 22 '19 at 15:23
  • I don't understand how that is supposed to address what I just said. You claimed the executor is kept because of references from held by a nested class. That's spurious reasoning. If that were true, no class containing a nested class could ever be GC'd. – Michael Feb 22 '19 at 15:34
  • If there was a reference to the nested class then yes, the non-static outer class would never be GC'd. The thread is a GC root, right? The thread is still running. The thread's stack frame _includes_ the `Worker` and the TPE instance. I think it can't be GC'd @Michael. – Gray Feb 22 '19 at 15:37
  • 1
    To clarify: The reference from the worker to the executor is not itself the problem. The problem is that the JVM (somewhere) has references to its running threads, and those references aren't cleared until (at least) the thread completes. The reference from the worker to the executor is important because that creates the path from the JVM to the executor. – Thomas Bitonti Feb 22 '19 at 17:39
  • I'm not sure there is a "problem" @ThomasBitonti but in terms of collecting the TPE, like you mention, the `Worker` is the reference link btw the Thread and the TPE. – Gray Feb 23 '19 at 15:15
  • Sorry if I was confusing. Better would be to say “the key reference which prevents garbage collection” instead “the problem”. I was putting more focus on the reference held by the JVM. (The reference from threads to an executor is also necessary.). I added that focus because another response was in reference to circular pointer references, and how those don’t prevent GC, which seemed to indicate that the JVM reference was overlooked. – Thomas Bitonti Feb 24 '19 at 03:15