Search code examples
javamultithreadingconcurrencyexecutorservicevolatile

If a thread finishes executing its last statement, as soon as that is done, is it guaranteed to dead as if explicitly calling return?


For example take this code snippet,

static volatile boolean isDone = false;
public static void main(String[] args) throws InterruptedException {
    // I know ArrayBlockingQueue cannot take 0 as the constructor arg, but please for the sake of example, let's pretend this was legal
    final ExecutorService exec = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(0));
    exec.execute(() -> {
        isDone = true;
        // does the compiler add a return statement here?
    });
    while (!isDone){
    }
    try {
        exec.execute(() -> {});
    } catch (RejectedExecutionException e) {
        System.out.println("What if this might actually happen in a super bad timing by the scheduler??");
    }
    exec.shutdown();
}

It might seem like the case that the first Thread created by the ExecutorService is dead, but is that really the case?

In the Thread Api https://docs.oracle.com/javase/6/docs/api/java/lang/Thread.html it says that if the run() method returns then the Thread is dead, but I don't explicitly call a return and I'm not sure what the Runtime or the compiler actually does.

For example it might implicitly add a return statement after isDone = true and before the enclosing } such as

exec.execute(() -> {
    isDone = true;
    return;
});

However, there might be some delay before actually hitting the return, since this is up to the scheduler and so the the next submitted runnable to the executor might get Rejected if the scheduler decided not to run that return statement before doing exec.execute(() -> {}); in the try block.


Solution

  • There is no technical difference between exec.execute(() -> isDone = true); and exec.execute(() -> { isDone = true; return; });

    However, this doesn’t say anything about the ExecutorService. Besides the fact, that an arbitrary amount of time may pass after observably completing the write to isDone and the execution of the return; statement, even a completion of your code doesn’t guaranty that the ExecutorService is ready to pick up a new job.

    When you call execute or submit and the number of worker threads has reached the number of specified core threads, it will only succeed if the offer call on the queue succeeds. In the case of SynchronousQueue, it will only succeed, if a worker thread has invoked take() on the queue already. Having returned from your code is not the same as having already invoked take() on the queue.

    This doesn’t even change when you use a future to ensure the completion of your job, e.g.

    final ExecutorService exec = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
        new SynchronousQueue<Runnable>() {
            @Override
            public Runnable take() throws InterruptedException {
                System.out.println(".take()");
                return super.take();
            }
        });
    Future<?> f = exec.submit(() -> { return; });
    f.get();
    try {
        exec.execute(() -> {});
    } catch (RejectedExecutionException e) {
        System.err.println(
            "What if this might actually happen in a super bad timing by the scheduler??");
    }
    exec.shutdown();
    

    does occasionally fail on my machine. The print statement is enough to slow down the operation to let the initiating thread sometimes overtake.

    The Future doesn’t help, as when get() returns, you know for sure that your code has been completed, as its caller, the FutureTask did already complete the future, however, this is not the same as having the worker thread already invoked take() on the queue or more precisely, having reached the point within the take() method that is sufficient to let the offer() call succeed.

    The same problem would apply to a bounded queue that is full, where the next offer() call can only succeeded, if a worker thread’s take() call removes an element before the offer() call.

    When you use ThreadPoolExecutor specifically, you could get the queue and use offer manually, to notice when the executor is not ready yet.

    However, the general solution is not to use thread counts or queue capacities that make your application dependent on subtle thread scheduling or execution timing detail. Or use a different RejectedExecutionHandler than ThreadPoolExecutor.AbortPolicy.