Is there a way to pause and resume virtual threads in Java?

105 views Asked by At

I'm working on a Java program that uses virtual threads to perform a specific task (a serial communication procedure with jssc). I would like to know if there is a way to pause and resume these virtual threads based on user input. I've read something about 'continuation' but it doesn't seem to be usable yet.

For example, I want the procedure to be paused when the user presses a "pause" button on the program's interface, and it should resume when the user clicks a "play" button.

I'm using Java 21 and I'm not currently using any specific libraries or frameworks for thread management.

Thank you in advance!

1

There are 1 answers

1
Basil Bourque On

Yes, locks & semaphores work the same

Virtual threads behave the same with regard to locks & semaphores. So you can use the same approach with virtual threads as with platform threads.

To quote JEP 444:

The primitive API to support locking, java.util.concurrent.LockSupport, now supports virtual threads … enables all APIs that use it (Locks, Semaphores, blocking queues, etc.) to park gracefully when invoked in virtual threads.

Better than pausing: Discard virtual thread

However, you may be missing out on the benefits of virtual threads.

The reason for their invention was to provide cheap threads, meaning very little impact on memory, CPU, JVM, or host OS. Virtual threads are meant to be like facial tissues: When needed, grab a fresh new one, use, and dispose. So rather than pause & resume a virtual thread, consider just disposing of the current one, then launch another when needed again.

Example app

Here is an example app that does just that. When launched, this app beeps once a second. On the console, the user can pause the beeping, and resume the beeping. Or so the user thinks. Pausing really lets the current virtual thread making the sound end its run, and die. Resuming actually launches a new fresh virtual thread to begin again with the beep-per-second.

In modern Java we rarely need to address the Thread class directly. Generally best to let an executor service juggle the threads. Here we choose to use an executor service backed by virtual threads.

Notice that an executor service is AutoCloseable. This means we can use try-with-resources syntax to automatically end the executor service.

We use a thread-safe collection here to act as a log, gathering feedback during the execution of the app. This is done rather than calling System.out.println directly as output from those calls does not necessarily appear chronologically on the console when made across threads.

package work.basil.example.threading;

import java.time.Duration;
import java.time.Instant;
import java.util.Scanner;
import java.util.SequencedCollection;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;

public class Pausing
{
    static final SequencedCollection < String > log = new CopyOnWriteArrayList <>( );

    public static void main ( String[] args )
    {
        Pausing app = new Pausing( );
        app.demo( );
    }

    private void demo ( )
    {
        log.add( "INFO - starting `demo` method at " + Instant.now( ) + " in thread id: " + Thread.currentThread( ).threadId( ) + " thread name: " + Thread.currentThread( ).getName( ) );
        final AtomicReference < BeepingCondition > beepingStatus = new AtomicReference <>( BeepingCondition.BEEPING );
        try (
                ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor( ) ;
        )
        {
            Future < ? > beepingFuture = executorService.submit( new Beeper( beepingStatus ) );

            Scanner scanner = new Scanner( System.in );
            System.out.println( "Beeper demo has commenced. Hear a beep every second." );
            while ( beepingStatus.get( ) != BeepingCondition.ENDED )
            {
                System.out.println( "To pause the beeping, type 'pause'. To restart beeping, 'beep'. To end this program, 'stop'. " );
                String input = scanner.nextLine( ).toLowerCase( ); // Blocks here until a line sent by user.
                log.add( "INFO - User intered input into `demo` method at " + Instant.now( ) + " in thread id: " + Thread.currentThread( ).threadId( ) + " thread name: " + Thread.currentThread( ).getName( ) + " Input: " + input );
                switch ( input )
                {
                    case "beep" ->
                    {
                        if ( beepingStatus.get( ) == BeepingCondition.BEEPING )
                        {
                            // Do nothing. Already beeping.
                        } else // Else not currently beeping. Start beeping again.
                        {
                            beepingFuture = executorService.submit( new Beeper( beepingStatus ) );
                            beepingStatus.set( BeepingCondition.BEEPING );
                        }
                    }
                    case "pause" ->
                    {
                        if ( beepingStatus.get( ) == BeepingCondition.PAUSED )
                        {
                            // Do nothing. Already paused.
                        } else // Else not currently beeping. Start beeping again.
                        {
                            beepingFuture.cancel( true );
                            beepingStatus.set( BeepingCondition.PAUSED );
                        }
                    }
                    case "stop" ->
                    {
                        beepingFuture.cancel( true );
                        beepingStatus.set( BeepingCondition.ENDED );
                    }
                    default ->
                    {
                        System.out.println( "Your input is unexpected. Try again. " );
                    }
                }

            }
        }
        log.add( "INFO - ending `demo` method at " + Instant.now( ) + " in thread id: " + Thread.currentThread( ).threadId( ) + " thread name: " + Thread.currentThread( ).getName( ) );
        log.forEach( System.out :: println );
    }

    static class Beeper implements Runnable
    {
        private final AtomicReference < BeepingCondition > keepOnBeeping;

        public Beeper ( final AtomicReference < BeepingCondition > continueBeeping )
        {
            this.keepOnBeeping = continueBeeping;
        }

        @Override
        public void run ( )
        {
            log.add( "INFO - starting `run` method at " + Instant.now( ) + " in thread id: " + Thread.currentThread( ).threadId( ) + " thread name: " + Thread.currentThread( ).getName( ) );
            while ( this.keepOnBeeping.get( ) != BeepingCondition.ENDED )
            {
                java.awt.Toolkit.getDefaultToolkit( ).beep( );
                try { Thread.sleep( Duration.ofSeconds( 1 ) ); } catch ( InterruptedException e )
                {
                    log.add( "INFO - `run` method interrupted at " + Instant.now( ) + " in thread id: " + Thread.currentThread( ).threadId( ) + " thread name: " + Thread.currentThread( ).getName( ) );
                    break; // Bail out of this `while` loop.
                }
            }
            log.add( "INFO - ending `run` method at " + Instant.now( ) + " in thread id: " + Thread.currentThread( ).threadId( ) + " thread name: " + Thread.currentThread( ).getName( ) );
        }
    }

    enum BeepingCondition
    { BEEPING, PAUSED, ENDED }
}

When run:

Beeper demo has commenced. Hear a beep every second.
To pause the beeping, type 'pause'. To restart beeping, 'beep'. To end this program, 'stop'. 
pause
To pause the beeping, type 'pause'. To restart beeping, 'beep'. To end this program, 'stop'. 
beep
To pause the beeping, type 'pause'. To restart beeping, 'beep'. To end this program, 'stop'. 
pause
To pause the beeping, type 'pause'. To restart beeping, 'beep'. To end this program, 'stop'. 
pause
To pause the beeping, type 'pause'. To restart beeping, 'beep'. To end this program, 'stop'. 
beep
To pause the beeping, type 'pause'. To restart beeping, 'beep'. To end this program, 'stop'. 
stop
INFO - starting `demo` method at 2024-03-27T00:53:53.536196Z in thread id: 1 thread name: main
INFO - starting `run` method at 2024-03-27T00:53:53.554782Z in thread id: 21 thread name: 
INFO - User intered input into `demo` method at 2024-03-27T00:54:00.825815Z in thread id: 1 thread name: main Input: pause
INFO - `run` method interrupted at 2024-03-27T00:54:00.832929Z in thread id: 21 thread name: 
INFO - ending `run` method at 2024-03-27T00:54:00.833298Z in thread id: 21 thread name: 
INFO - User intered input into `demo` method at 2024-03-27T00:54:06.124971Z in thread id: 1 thread name: main Input: beep
INFO - starting `run` method at 2024-03-27T00:54:06.125380Z in thread id: 30 thread name: 
INFO - User intered input into `demo` method at 2024-03-27T00:54:12.881055Z in thread id: 1 thread name: main Input: pause
INFO - `run` method interrupted at 2024-03-27T00:54:12.881251Z in thread id: 30 thread name: 
INFO - ending `run` method at 2024-03-27T00:54:12.881301Z in thread id: 30 thread name: 
INFO - User intered input into `demo` method at 2024-03-27T00:54:15.165664Z in thread id: 1 thread name: main Input: pause
INFO - User intered input into `demo` method at 2024-03-27T00:54:17.341976Z in thread id: 1 thread name: main Input: beep
INFO - starting `run` method at 2024-03-27T00:54:17.342322Z in thread id: 32 thread name: 
INFO - User intered input into `demo` method at 2024-03-27T00:54:21.753798Z in thread id: 1 thread name: main Input: stop
INFO - `run` method interrupted at 2024-03-27T00:54:21.754009Z in thread id: 32 thread name: 
INFO - ending `run` method at 2024-03-27T00:54:21.754039Z in thread id: 32 thread name: 
INFO - ending `demo` method at 2024-03-27T00:54:21.754048Z in thread id: 1 thread name: main

For more info, see What does java.lang.Thread.interrupt() do?.

If we follow strictly our concept of disposable virtual threads, then we do not actually need the AtomicReference used to signal between threads. Instead, we can simplify our code by relying upon the interrupt feature of threads available here via Future#cancel.

package work.basil.example.threading;

import java.time.Duration;
import java.time.Instant;
import java.util.Scanner;
import java.util.SequencedCollection;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Pausing
{
    static final SequencedCollection < String > log = new CopyOnWriteArrayList <>( );

    public static void main ( String[] args )
    {
        Pausing app = new Pausing( );
        app.demo( );
    }

    private void demo ( )
    {
        log.add( "INFO - starting `demo` method at " + Instant.now( ) + " in thread id: " + Thread.currentThread( ).threadId( ) + " thread name: " + Thread.currentThread( ).getName( ) );
        try (
                ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor( ) ;
        )
        {
            Future < ? > beepingFuture = executorService.submit( new Beeper( ) );

            Scanner scanner = new Scanner( System.in );
            System.out.println( "Beeper demo has commenced. Hear a beep every second." );
            String input = "";
            do
            {
                System.out.println( "To pause the beeping, type 'pause'. To restart beeping, 'beep'. To end this program, 'stop'. " );
                input = scanner.nextLine( ).toLowerCase( ); // Blocks here until a line sent by user.
                log.add( "INFO - User intered input into `demo` method at " + Instant.now( ) + " in thread id: " + Thread.currentThread( ).threadId( ) + " thread name: " + Thread.currentThread( ).getName( ) + " Input: " + input );
                switch ( input )
                {
                    case "beep" -> beepingFuture = executorService.submit( new Beeper( ) );
                    case "pause" , "stop" -> beepingFuture.cancel( true );
                    default -> System.out.println( "Your input is unexpected. Try again. " );
                }
            }
            while ( !input.equalsIgnoreCase( "stop" ) );
        }
        log.add( "INFO - ending `demo` method at " + Instant.now( ) + " in thread id: " + Thread.currentThread( ).threadId( ) + " thread name: " + Thread.currentThread( ).getName( ) );
        log.forEach( System.out :: println );
    }

    static class Beeper implements Runnable
    {
        @Override
        public void run ( )
        {
            log.add( "INFO - starting `run` method at " + Instant.now( ) + " in thread id: " + Thread.currentThread( ).threadId( ) + " thread name: " + Thread.currentThread( ).getName( ) );
            while ( ! Thread.currentThread( ).isInterrupted( ) )
            {
                java.awt.Toolkit.getDefaultToolkit( ).beep( );
                try { Thread.sleep( Duration.ofSeconds( 1 ) ); } 
                catch ( InterruptedException e )
                {
                    log.add( "INFO - `run` method interrupted while sleeping at " + Instant.now( ) + " in thread id: " + Thread.currentThread( ).threadId( ) + " thread name: " + Thread.currentThread( ).getName( ) );
                    break; // Bail out of this `while` loop.
                }
            }
            log.add( "INFO - ending `run` method at " + Instant.now( ) + " in thread id: " + Thread.currentThread( ).threadId( ) + " thread name: " + Thread.currentThread( ).getName( ) );
        }
    }
}

In the demo method, we changed the while loop to a do - while.

In the nested Beeper task class, we change the while test to while ( ! Thread.currentThread( ).isInterrupted( ) ) rather than check our AtomicReference.

When run:

Beeper demo has commenced. Hear a beep every second.
To pause the beeping, type 'pause'. To restart beeping, 'beep'. To end this program, 'stop'. 
pause
To pause the beeping, type 'pause'. To restart beeping, 'beep'. To end this program, 'stop'. 
elephant
Your input is unexpected. Try again. 
To pause the beeping, type 'pause'. To restart beeping, 'beep'. To end this program, 'stop'. 
beep
To pause the beeping, type 'pause'. To restart beeping, 'beep'. To end this program, 'stop'. 
pause
To pause the beeping, type 'pause'. To restart beeping, 'beep'. To end this program, 'stop'. 
stop
INFO - starting `demo` method at 2024-03-27T01:06:18.525643Z in thread id: 1 thread name: main
INFO - starting `run` method at 2024-03-27T01:06:18.543738Z in thread id: 21 thread name: 
INFO - User intered input into `demo` method at 2024-03-27T01:06:21.856024Z in thread id: 1 thread name: main Input: pause
INFO - `run` method interrupted while sleeping at 2024-03-27T01:06:21.860075Z in thread id: 21 thread name: 
INFO - ending `run` method at 2024-03-27T01:06:21.860305Z in thread id: 21 thread name: 
INFO - User intered input into `demo` method at 2024-03-27T01:06:28.206060Z in thread id: 1 thread name: main Input: elephant
INFO - User intered input into `demo` method at 2024-03-27T01:06:32.787616Z in thread id: 1 thread name: main Input: beep
INFO - starting `run` method at 2024-03-27T01:06:32.788238Z in thread id: 29 thread name: 
INFO - User intered input into `demo` method at 2024-03-27T01:06:37.446688Z in thread id: 1 thread name: main Input: pause
INFO - `run` method interrupted while sleeping at 2024-03-27T01:06:37.447089Z in thread id: 29 thread name: 
INFO - ending `run` method at 2024-03-27T01:06:37.447125Z in thread id: 29 thread name: 
INFO - User intered input into `demo` method at 2024-03-27T01:06:40.642388Z in thread id: 1 thread name: main Input: stop
INFO - ending `demo` method at 2024-03-27T01:06:40.642760Z in thread id: 1 thread name: main

Continuations — merely an implementation detail

As for your mention of continuations, that is a lower-level implementation detail behind virtual threads. The Project Loom team has considered exposing their continuation framework to Java programmers, but have not yet decided to do so.

If you are curious about how Loom uses continuations to make Java’s virtual threads so very fast and low-impact, see an interesting 2023-08 talk by Project Loom technical lead, Ron Pressler.

But understand that learning about continuations is entirely optional. Knowing about continuations is unnecessary for making practical use of virtual threads in Java.


Caveat: I am not an expert on Java concurrency. So do your own investigation before relying upon my advice here.