Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java Thread interrupt non deterministic behaviour

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";
    }));
like image 909
Sciotherium Avatar asked Jun 11 '26 12:06

Sciotherium


1 Answers

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.

like image 71
Slaw Avatar answered Jun 13 '26 03:06

Slaw



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!