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,finalize
It 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…