I was going through Java Concurrency In Practice and got stuck at the 8.3.1 Thread creation and teardown topic. The following footnote warn
While running this program in jdk 1.5,1.6,1.7 and 1.8, I found different implementations of ThreadPoolExecutor#execute(Runnable)
in 1.5,1.6 and 1.7+. Here's what I found:
JDK 1.5 implementation
//Here poolSize is the number of core threads running.
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
for (;;) {
if (runState != RUNNING) {
reject(command);
return;
}
if (poolSize < corePoolSize && addIfUnderCorePoolSize(command))
return;
if (workQueue.offer(command))
return;
Runnable r = addIfUnderMaximumPoolSize(command);
if (r == command)
return;
if (r == null) {
reject(command);
return;
}
// else retry
}
}
This implementation does not create a thread when corePoolSize
is 0, therefore the supplied task does not execute.
JDK 1.6 implementation
//Here poolSize is the number of core threads running.
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
else if (!addIfUnderMaximumPoolSize(command))
reject(command); // is shutdown or saturated
}
}
JDK 1.6 creates a new thread even if the corePoolSize
is 0.
JDK 1.7+ implementation(Similar to JDK 1.6 but with better locks and state checks)
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
JDK 1.7 too creates a new thread even if the corePoolSize
is 0.
So, it seems that corePoolSize=0
is a special case in each versions of JDK 1.5 and JDK 1.6+.
But it is strange that the book's explanation doesn't match any of the program results.
Seems like it was a bug with older java versions but it doesn't exist now in Java 1.8.
According to the Java 1.8 documentation from ThreadPoolExecutor.execute()
:
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
* ....
*/
In the second point, there is a recheck after adding a worker to the queue that if instead of queuing the task, a new thread can be started, than rollback the enqueuing and start a new thread.
This is what is happening. During first check the task is queued but during recheck, a new thread is started which executes your task.
This odd behavior of ThreadPoolExecutor
in Java 5 when the core pool size is zero was apparently recognized as a bug and quietly fixed in Java 6.
Indeed, the problem reappeared in Java 7 as a result of some code reworking between 6 and 7. It was then reported as a bug, acknowledged as a bug and fixed.
Either way, you should not be using a version of Java that is affected by this bug. Java 5 was end-of-life in 2015, and the latest available versions of Java 6 and later are not affected. That section of "Java Concurrency In Practice" is no longer apropos.
References:
ThreadPoolExecutor
in the JSR166y bundle.)