A few days ago, I was helping my colleagues troubleshoot an accidental thread pool error on the production line
The logic is simple. The thread pool performs an asynchronous task with results. However, there have been occasional errors recently:
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]
All the simulation code problems in this article have been simulated & under HotSpot java8 (1.8.0_221)
The following is the simulation code through executors Newsinglethreadexecution creates a single thread thread pool, and then obtains the Future results at the caller:
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 sub thread keeps gc, simulating the occasional gc new Thread(new Runnable() { @Override public void run() { while (true) { System.gc(); } } }).start(); } /** * Execute tasks asynchronously * @return */ public Future<String> submit() { //Key points, via executors Newsinglethreadexecution creates a single threaded 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); return futureTask; } }
Analysis & questions
The first question to think about is: why is the thread pool closed? There is no place in the code to close it manually. Take a look at executors The source code implementation of newsinglethreadexecotor:
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
What is created here is actually a FinalizableDelegatedExecutorService. This wrapper class overrides the finalize function, that is, this class will execute the shutdown method of the thread pool before being recycled by GC.
The problem is that GC will only recycle unreachable objects. executorService should be reachable before the stack frame of the submit function is executed and out of the stack.
For this problem, first throw out the conclusion:
finalize may also be performed when the object still exists in the stack frame
There is an introduction to finalize in the oracle jdk document:
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 continuously from any potential of any active thread; The java compiler or code generator may set the object that is no longer accessed to null in advance, so that the object can be recycled in advance.
In other words, under the optimization of the jvm, the object may be emptied and recycled in advance after it is unreachable.
Take an example to verify it (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 results Created A@1be6f5c3 A@1be6f5c3 was finalized!//finalize method output done.
As can be seen from the example, if a is no longer used after the loop is completed, it will execute finalize first; Although from the object scope, the method is not completed and the stack frame is not out of the stack, it will still be executed in advance.
Now add a line of code, print object a on the last line, and let the compiler / code generator think that there is a reference to object a.
... System.out.println(a); //Print results Created A@1be6f5c3 done. A@1be6f5c3
From the results, the finalize method is not executed (because the process ends directly after the main method is executed), and there will be no problem of finalizing in advance
Based on the above test results, test another case, set object a to null before the loop, and print and hold the reference of object a 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 results Created A@1be6f5c3 A@1be6f5c3 was finalized! done. null
As a result, if you manually set null, the object will also be recycled in advance. Although there is a reference at the end, the reference is also null at this time.
Now return to the thread pool problem above. According to the mechanism described above, after analyzing that there is no reference, the object will be finalize d in advance
In the above code, the executorservice with reference is clearly before return Execute (future ask), why do you finalize it in advance?
It is speculated that in the execute method, threadPoolExecutor will be called, and a new thread will be created and started. At this time, an active thread switching will occur, resulting in the unreachable object in the active thread
Combined with the description in the Oracle Jdk document above, "reachable objects are any object that can be accessed continuously from any active thread". It can be considered that the object is considered unreachable due to a displayed thread switch, resulting in the thread pool being finalize d in advance
Let's test the conjecture:
//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(); return null; } } //Executors.java, simulating juc's executors public class Executors { /** * Simulate 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 modify 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, simulating the ThreadPoolExecutor of juc 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 and simulate ThreadPoolExecutor execute new Thread(new Runnable() { @Override public void run() { } }).start(); //Simulate the ThreadPoolExecutor. After starting a new thread, cycle to check the thread pool status and verify whether it will be shut down in finalize //If the thread pool is shut down in advance, 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); } }
An error is reported after several times of execution:
Exception in thread "Thread-1" java.lang.RuntimeException: reject!!![true]
In terms of errors, the "thread pool" is also shut down in advance. Must it be due to the new thread?
Next, modify the new thread to thread Sleep test:
//TThreadPoolExecutor.java, modified execute method public void execute() { try { //Explicit sleep 1 ns, active thread switching TimeUnit.NANOSECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } //Simulate the ThreadPoolExecutor. After starting a new thread, cycle to check the thread pool status to verify whether it will be shut down in finalize //If the thread pool is shut down in advance, an exception is thrown for (int i = 0; i < 1_000_000; i++) { if(ctl.get()){ throw new RuntimeException("reject!!!["+ctl.get()+"]"); } } }
The execution result is the same as an error:
Exception in thread "Thread-3" java.lang.RuntimeException: reject!!![true]
Thus, if an explicit thread switch occurs during execution, the compiler / code generator will think that the outer wrapper object is unreachable
summary
Although GC only recycles objects that cannot reach GC ROOT, under the optimization of compiler (not explicitly stated, it may also be JIT) / code generator, the object may be set to null in advance, or "early object unreachable" caused by thread switching may occur.
Therefore, if you want to do something in the finalize method, you must reference the object in the last display (toString/hashcode can be used) to maintain the reachability of the object
The object inaccessibility caused by thread switching above is not supported by official literature. It is just a personal test result. If there is a problem, please point it out
To sum up, this recycling mechanism is not a JDK bug, but an optimization strategy, just recycling in advance; But executors There is a bug in the implementation of newsinglethreadexecutor to automatically close the thread pool through finalize. After optimization, the thread pool may be shut down in advance, resulting in exceptions.
The problem of thread pool is also an open but unresolved problem in the JDK forum https://bugs.openjdk.java.net/browse/JDK-8145304 .
However, under JDK11, the problem has been fixed:
JUC Executors.FinalizableDelegatedExecutorService public void execute(Runnable command) { try { e.execute(command); } finally { reachabilityFence(this); } }