2

I have this piece of code in Java and I tried it out on Java 21 (Eclipse Temurin and GraalVM)

public static void main(String[] args) {
        Thread.currentThread().interrupt();

        long start = System.currentTimeMillis();
        System.out.println("started measuring ...");

        int i = 0;

        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            List<Future<?>> futures = new ArrayList<>();
            futures.add(executor.submit(() -> "api1"));
            futures.add(executor.submit(() -> "api2"));
            futures.add(executor.submit(() -> "api3"));
            for (; i < futures.size(); i++) {
                System.out.println((i + 1) + " " + futures.get(i).get());
            }
        } catch (ExecutionException e) {
            System.out.println("error: " + e.getCause());
            throw new RuntimeException(e);
        } catch (InterruptedException e) {
            System.out.println("interrupted after " + (System.currentTimeMillis() - start) + " at " + (i + 1));
            throw new RuntimeException(e);
        }
    }

Sometimes the main thread does not get interrupted, sometimes it does. So the output is either:

started measuring ...
interrupted after 0 at 1
Exception in thread "main" java.lang.RuntimeException: java.lang.InterruptedException
        at rs.sf.App.main(App.java:70)
Caused by: java.lang.InterruptedException
        at java.base/java.util.concurrent.FutureTask.awaitDone(FutureTask.java:471)
        at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:190)
        at rs.sf.App.main(App.java:63)

Or:

started measuring ...
1 api1
2 api2
3 api3

I couldn't find what's the reason for this non-deterministic behavior. I thought that the first statement (the main thread setting the interrupt flag on itself) is executed before any other subsequent line of code and should therefore the execution should always result in InterruptedException

EDIT:

If I write futures with a Thread.sleep, the behavior becomes deterministic and the InterruptedException is always thrown, i.e. like this:

futures.add(executor.submit(() -> {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return "api1";
    }));
3
  • Random guess: FutureTask.get(...) does not check the interrupt flag if there already is a result available (the fact of wether a result is available depends on how the JVM and OS schedules the main thread and the carrier threads, and is not guranteed to follow any rules whatsoever) Commented Oct 24 at 14:29
  • 4
    Future::get() says it throws an InterruptedException "if the current thread was interrupted while waiting". If all the futures are complete by the time you call get on any of them, then it's legal for them to return the result instead of throwing an interrupted exception. Commented Oct 24 at 14:31
  • Hello Stefan, "I thought that the first statement (the main thread setting the interrupt flag on itself) is executed before any other subsequent line of code", It seems not. Try adding after Thread.currentThread().interrupt(); the line Thread.sleep( 100 );. Commented Oct 24 at 14:33

2 Answers 2

6

In your code, the main thread is always interrupted before calling get() on any of the futures. The non-deterministic output you're seeing is due to other factors.

From the documentation of Future::get():

Waits if necessary [emphasis added] for the computation to complete, and then retrieves its result.

Returns:

the computed result

Throws:

CancellationException - if the computation was cancelled

ExecutionException - if the computation threw an exception

InterruptedException - if the current thread was interrupted while waiting [emphasis added]

That means it's legal for get() to ignore the interrupt status of the calling thread if the Future is already done. Therefore the output of your code depends on how the threads are scheduled, which is non-deterministic. In other words, you have a race condition.

Your tasks are very short. Thus, it's plausible for all of them to finish by the time the main thread calls get() on any of the futures. In that case you may not see an InterruptedException. But sometimes at least one task will not finish before the main thread calls get() on its future. And that's when you will see an InterruptedException.

The fact adding a call to sleep in the tasks seemingly guarantees an InterruptedException supports this. It gives time for the main thread to call get() on one of the futures before they all finish. But it's still not deterministic; see John Bollinger's answer.


The ExecutorService returned by newVirtualThreadPerTaskExecutor() makes use of FutureTask. You can see its implementation (at least in contemporary versions) doesn't check the interrupt status of the calling thread if the future is already done.

Sign up to request clarification or add additional context in comments.

Comments

5

I couldn't find what's the reason for this non-deterministic behavior.

Simple: you have a race between your tasks completing and your main thread requesting their results.

If a Future's value is already available when the main thread requests it then it is successfully retrieved. If not then, given that the main thread's interrupt status is set, attempting to retrieve the Future's value causes an InterruptedException to be thrown.

I thought that the first statement (the main thread setting the interrupt flag on itself) is executed before any other subsequent line of code

Yes, in that thread.

and should therefore the execution should always result in InterruptedException

No, Future.get() does not guarantee that. Slaw's answer explains this in more detail.

EDIT:

If I write futures with a Thread.sleep, the behavior becomes deterministic

No, it does not.

You still have exactly the same race as before, but in this case you have put your thumb on the scale to make it exceedingly unlikely (but not impossible) that the main thread loses to any of the executor threads. That made your observations of the behavior consistent, but that's not the same as deterministic.

I raise this in part because it is unfortunately common for inexperienced programmers to mistake methods for producing timing effects, such as Thread.sleep(), to be safe and effective for addressing synchronization problems. They are not.

4 Comments

Hello Teacher, It is very likely that I have misinterpreted the question, but what I understood was that the OP wants an exception to be thrown (hence the call to Thread.currentThread().interrupt(); in the first line), but since it's not immediate, sometimes the threads get executed, which is what he calls “non-deterministic behavior.” However, as we know, there's no such thing as “non-deterministic” behavior in machine hardware (except for hardware failures); every logical ‘1’ and “0” has a reason for being there.
The OP expresses confusion about the program is inconsistent with respect to whether an InterruptedException is thrown in the main thread. Whether that's what they wanted or expected of the program is unspecified. They attribute the inconsistency to indeterminism, which is a perfectly fine thing to do at the level of the Java language. It cannot be determined from the Java language spec and the specifications of the various classes involved whether such an exception should be thrown.
Since the outcome depends on the thread scheduling, we’d have to enumerate all possible influences on all relevant hardware configs and operating systems. This includes events like user input which may cause interrupts plus other processes/threads being rescheduled to handle the input and subtle timing issues which in turn even includes the room temperature as an influence, as most CPU can change their clock speed when they get too hot (or change it in smaller steps even before it gets too hot). Given those possible influences, I consider the term “non-deterministic“ justified.
“there is no such thing as ‘non-deterministic’ behavior” is not entirely correct. John is right in that in some areas, a certain language can have non-deterministic behaviors (usually associated with misuse). My statement should have been limited to this particular case, in which the first line of the method calls ** Thread.currentThread().interrupt(); ** is called, so one interprets that nothing after that call can be executed. If it does, the only possibility I see is that the “startup” of interrupt() comes after the calls to the other threads, and yes, I totally agree.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.