Problem description

A few days ago, I was helping my colleague to troubleshoot an occasional thread pool error

The logic is simple: the thread pool performs an asynchronous task with a result. But recently there have been occasional errors:

java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@a5acd19 rejected from java.util.concurrent.ThreadPoolExecutor@30890a38[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
Copy the code

The emulated code in this article has been emulated under HotSpot Java8 (1.8.0_221)

. Below is a simulation code, through the Executors newSingleThreadExecutor create a single thread thread pool, and then the caller to obtain the result of the Future

public class ThreadPoolTest {

    public static void main(String[] args) {
        final ThreadPoolTest threadPoolTest = new ThreadPoolTest();
        for (int i = 0; i < 8; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {

                        Future<String> future = threadPoolTest.submit();
                        try {
                            String s = future.get();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } catch (ExecutionException e) {
                            e.printStackTrace();
                        } catch (Error e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }
        
        // The child thread does not stop gc, simulating occasional GC
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.gc();
                }
            }
        }).start();
    }

    /** * Perform tasks asynchronously * @return */
    public Future<String> submit() {
        / / key point, through the Executors. NewSingleThreadExecutor create a single thread thread pool
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        FutureTask<String> futureTask = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception {
                Thread.sleep(50);
                return System.currentTimeMillis() + ""; }}); executorService.execute(futureTask);returnfutureTask; }}Copy the code

Analysis & Questions

The first question to ponder is: why is the thread pool closed? There is no place in the code to shut it down manually. Look at the Executors. NewSingleThreadExecotor source code to achieve:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1.1.0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>()));
}
Copy the code

Created here is actually a FinalizableDelegatedExecutorService, the wrapper class overrides the finalize function, that is to say, this class will before being GC recycling, to perform the shutdown method of thread pool.

The problem is that the GC only reclaims unreachable objects, and the executorService should be reachable until the submit function’s stack frame runs off the stack.

To solve this problem, first throw out the conclusion:

While the object is still in the stack frame,finalizeIt may be executed

The Oracle JDK documentation has a passage about Finalize:

docs.oracle.com/javas…

A reachable object is any object that can be accessed in any potential continuing computation from any live thread.

Optimizing transformations of a program can be designed that reduce the number of objects that are reachable to be less than those which would naively be considered reachable. For example, a Java compiler or code generator may choose to set a variable or parameter that will no longer be used to null to cause the storage for such an object to be potentially reclaimable sooner.

A reachable object is any object that can be accessed potentially continuously from any active thread. The Java compiler or code generator may prenull objects that are no longer accessible, allowing them to be reclaimed early

That is, under JVM optimization, objects that are unreachable can be pre-empted and reclaimed

As an example to verify (from https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope) :

class A {
    @Override protected void finalize() {
        System.out.println(this + " was finalized!");
    }

    public static void main(String[] args) throws InterruptedException {
        A a = new A();
        System.out.println("Created " + a);
        for (int i = 0; i < 1_000_000_000; i++) {
            if (i % 1_000_00 = =0)
                System.gc();
        }
        System.out.println("done."); }}// Print the result
Created A@1be6f5c3
A@1be6f5c3 was finalized!// Finalize method output
done.
Copy the code

As can be seen from the example, if A is no longer used after the loop is completed, Finalize will be executed first. Although the method is not executed and the stack frame is not removed from the stack, it is still executed ahead of schedule from the object scope.

Now add a line of code that prints object A on the last line to make the compiler/code generator think there is a reference to object A behind it

. System.out.println(a);

// Print the result
Created A@1be6f5c3
done.
A@1be6f5c3
Copy the code

From the result, finalize methods are not executed (because the process ends directly after main method is executed), let alone finalize in advance

Based on the above test results, test the case where object A is set to NULL before looping and a reference to hold object A is printed at the end

A a = new A();
System.out.println("Created " + a);
a = null;// Manually set null
for (int i = 0; i < 1_000_000_000; i++) {
    if (i % 1_000_00 = =0)
        System.gc();
}
System.out.println("done.");
System.out.println(a);

// Print the result
Created A@1be6f5c3
A@1be6f5c3 was finalized!
done.
null
Copy the code

As a result, manually setting null also causes the object to be reclaimed prematurely, even though there is a reference at the end, but the reference is null


Now back to the thread pool problem above, according to the mechanism introduced above, objects will be prefinalized after the analysis has no references

Executorservice.execute (futureTask) is a reference to return, so why finalize it earlier?

This is probably because in the execute method, threadPoolExecutor is called, a new thread is created and started, and an active thread switch occurs, making objects unreachable in the active thread

A reachable object is any object that can be potentially continuously accessed from any active thread. If the object is considered unreachable after a thread switch, the thread pool is prefinalized

Let’s test the conjecture:

// the entry function
public class FinalizedTest {
    public static void main(String[] args) {
        final FinalizedTest finalizedTest = new FinalizedTest();
        for (int i = 0; i < 8; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        TFutureTask future = finalizedTest.submit();
                    }
                }
            }).start();
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.gc();
                }
            }
        }).start();
    }
    public TFutureTask submit(){
        TExecutorService TExecutorService = Executors.create();
        TExecutorService.execute();
        returnnull; }}//Executors. Java, simulation of JUC Executors
public class Executors {
    / * * * simulation Executors. CreateSingleExecutor * @ return * /
    public static TExecutorService create(){
        return new FinalizableDelegatedTExecutorService(new TThreadPoolExecutor());
    }

    static class FinalizableDelegatedTExecutorService extends DelegatedTExecutorService {

        FinalizableDelegatedTExecutorService(TExecutorService executor) {
            super(executor);
        }
        
        /** * execute shutdown in the destructor to change the thread pool state * @throws Throwable */@Override protected void finalize() throws Throwable { super.shutdown(); } } static class DelegatedTExecutorService extends TExecutorService { protected TExecutorService e; public DelegatedTExecutorService(TExecutorService executor) { this.e = executor; } @Override public void execute() { e.execute(); } @Override public void shutdown() { e.shutdown(); }}}/ / TThreadPoolExecutor. Java, simulate the juc ThreadPoolExecutor
public class TThreadPoolExecutor extends TExecutorService {

    /** * Thread pool status, false: not closed, true closed */
    private AtomicBoolean ctl = new AtomicBoolean();

    @Override
    public void execute() {
        / / start a new thread, simulated ThreadPoolExecutor. Execute
        new Thread(new Runnable() {
            @Override
            public void run() {

            }
        }).start();
        // simulate ThreadPoolExecutor. After starting a new thread, loop to check whether the thread pool will be shutdown in finalize
        // if the thread pool is shutdown prematurely, an exception is thrown
        for (int i = 0; i < 1_000_000; i++) {
            if(ctl.get()){
                throw new RuntimeException("reject!!! ["+ctl.get()+"]");
            }
        }
    }

    @Override
    public void shutdown() {
        ctl.compareAndSet(false.true); }}Copy the code

Error after some time:

Exception in thread "Thread-1"java.lang.RuntimeException: reject!!! [true]
Copy the code

The “thread pool” was also shutdown early, so it must have been a new thread?

Let’s test the new Thread by changing it to thread. sleep.

/ / TThreadPoolExecutor. Java, modified the execute method
public void execute() {
    try {
        // Explicitly sleep 1 ns, actively switch threads
        TimeUnit.NANOSECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // simulate ThreadPoolExecutor. After starting a new thread, loop to check whether the thread pool will be shutdown in finalize
    // if the thread pool is shutdown prematurely, an exception is thrown
    for (int i = 0; i < 1_000_000; i++) {
        if(ctl.get()){
            throw new RuntimeException("reject!!! ["+ctl.get()+"]"); }}}Copy the code

The execution result is also an error

Exception in thread "Thread-3"java.lang.RuntimeException: reject!!! [true]
Copy the code

It follows that if an explicit thread switch occurs during execution, the compiler/code generator will consider the wrapper object unreachable

conclusion

Although the GC will only reclaim objects that are unreachable to GC ROOT, under compiler (not explicitly, but possibly JIT)/code generator optimizations, objects can be nulled prematurely or “unreachable ahead of time” due to thread switches.

So if you want to do something with Finalize methods, you should always reference objects (toString/hashcode can be used) at the end and keep the objects reachable.

The above mentioned object unreachable caused by thread switch, no official literature support, just a personal test results, if any questions welcome to point out

This is not a BUG in the JDK, but rather an optimization strategy. But Executors. NewSingleThreadExecutor automatically closed by finalize in the implementation of the thread pool is buggy, may cause the thread pool after optimized shutdown ahead of time, resulting in abnormal.

Thread pool of the problem, in the BBS of the JDK is also a public but did not solve the problem of status at https://bugs.openjdk.java.net/browse/JDK-8145304.

In JDK11, however, this issue has been fixed:

JUC Executors.FinalizableDelegatedExecutorService public void execute(Runnable command) { try { e.execute(command); } finally { reachabilityFence(this); }}Copy the code

From: segmentfault.com/a/119000002…