0

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.

katiex7
  • 863
  • 12
  • 23
  • any method that completes `returns` - why should it be any different? – Scary Wombat Feb 09 '18 at 06:56
  • Yes I understand where you are coming from, but my question is how can you be sure when something has completed return? Because in the maybe possible case that isDone is true, and the runnable that the first execute runs has not returned, then the next task might be rejected. I want to know if this is possible – katiex7 Feb 09 '18 at 07:01
  • 1
    Use Thread.join – Scary Wombat Feb 09 '18 at 07:05
  • Upvoted because it might help others, although the reasons I want to know the answer to my questions are different – katiex7 Feb 09 '18 at 07:13

3 Answers3

1

Yes, the compiler acts as if the end of any method contains a return; statement.

No, your code isn't correct.

The problem is that two threads can interleave in any way possible. And as Murphy states, what can happen will (eventually) happen.

Imagine if the executor thread runs the isDone = true statement and then goes to sleep. The main thread wakes up and thinks the executor is done so it goes on starting a new executor. But the first one isn't completed yet (the invisible return hasn't executed (as well as internal code in the Java library that comes after).

The correct way to wait for a thread to finish is Thread.join() as stated in a comment, but you don't work with threads directly, you work with tasks. The correct way to wait for a task is Future.get(). To obtain a Future object of your task, change execute() to submit(). There doesn't seem much difference between them apart from that submit returns the Future you want. (See also What is the difference between submit and execute method with ThreadPoolExecutor)

public static void main(String[] args) throws InterruptedException {
    final ExecutorService exec = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
    Future<?> future = exec.submit(() -> {
        // Do work
    });

    try {
        future.get(); //blocking call
    } catch (ExecutionException e) {
        e.getCause().printStackTrace(); // The RuntimeException thrown in the lambda.
    }

    try {
        exec.submit(() -> {});
    } catch (RejectedExecutionException e) {
        // Shouldn't happen
    }
    exec.shutdown();
}

[EDIT]

And an executor with a queue of length 0 is actually possible:

new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
Mark Jeronimus
  • 9,278
  • 3
  • 37
  • 50
  • Thank you Mark, helpful examples. I already knew of the future.get but have never used a SynchronousQueue directly before. I've accepted this in faith that there is an implicit return statement and a possible gap between the isDone = true and the implicit return, thus isDone = true not being the last statement to be executed which would imply that the executor is not done with the first task – katiex7 Feb 09 '18 at 16:30
  • There is still a race possible. All that a successful return from `get()` guarantees, is that *your* code has been completed and the calling `FutureTask` has set its own result state. This does not prove that the calling worker thread did already reach the critical point within `take()` of the `SynchronousQueue` to make the next `offer()` succeed. I’ve [made an example](https://stackoverflow.com/a/48746675/2711488) that allows to observe that race even after `Future.get()` returned… – Holger Feb 12 '18 at 12:40
  • @Mark Jeronimus you should check out Holgers post its really worth the time, I would have never known a race condition was possible after Future.get but now I do – katiex7 Feb 13 '18 at 04:02
1

All method will end. No matter wether you explicitly call the return statement or not.

Just see the oracle documentation here. Returning a Value from a Method

A method returns to the code that invoked it when it completes all the statements in the method, reaches a return statement, or throws an exception (covered later), whichever occurs first.

In a word, you can considere the thread as a stack of statements. You can see the thread's stack through the DEBUG function in your IDE.

When you start a process(sth like *.exe in windows, etc.)

  1. The OS will load you program file into the memory.
  2. And look for the boot(The main method).
  3. Then PUSH the main method to the stack of Main Thread. So, you can even call the main method in any position of your code. Not only called through booting up.
  4. If all threads in a process ended. The process will exit. So, like many smart phone application, there will be a looping thread waiting for user interaction action. Thus the application will not exit even you do not perform any operations to it.
user207421
  • 305,947
  • 44
  • 307
  • 483
EI CHO
  • 86
  • 6
1

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.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • I had to read this a couple times, but wow you really have a indepth knowledge of the Executors framework. Thank you for sharing it!! – katiex7 Feb 13 '18 at 03:28