Search code examples
javamultithreadingconcurrency

How to make main wait for threads to execute, but these threads must be started at the same time


I have a program in which an array is created, cloned into the required number of the same arrays, then sorted in different ways.

https://github.com/zotov88/algorithms

public class SortingRunner {

    final static int CAPACITY = 10_000;
    final static int FROM = -100;
    final static int TO = 100;
    final static int COUNT_ARRAYS = 4;

    public static void main(String[] args) throws InterruptedException {

        int[][] arrays = Utils.generateTheSameRandomArrays(CAPACITY, FROM, TO, COUNT_ARRAYS);

         List<Sorting> sortingList = List.of(
                new BubbleSort(arrays[0]),
                new SelectionSort(arrays[1]),
                new MergeSort(arrays[2]),
                new QuickSort(arrays[3])
        );

        for (Sorting sorting : sortingList) {
            Thread t = new Thread(() -> Utils.speedTest(sorting));
            t.start();
            t.join();
        }

        Utils.printArrays(arrays);
    }
}

In this section of the code, the threads are first processed one by one, then the printing of arrays is started in the main.


        for (Sorting sorting : sortingList) {
            Thread t = new Thread(() -> Utils.speedTest(sorting));
            t.start();
            t.join();
        }

        Utils.printArrays(arrays);

I want the arrays to be sorted simultaneously in each thread, and the main waits for them to complete and then prints.


Solution

  • tl;dr

    Use an executor service.

    try (
            ExecutorService executorService = Executors. … ;
    )
    {
        futures = executorService.invokeAll ( tasks );
    }
    

    Executors framework

    The Executors framework was added to Java 5 to support this kind of work. Since then, we rarely need to interact with Thread directly.

    Define your tasks as Runnable or Callable objects. Submit them to an executor service object with your desired behaviors.

    Usually best to use the Executors utility class to instantiate an executor service. Here we go with an executor service that creates a new fresh virtual thread for each task.

    When submitting tasks, you get a Future object. Use that object to check on the progress of your task. And in the case of Callable, use that Future object to access the result of your task.

    You can submit tasks one at a time. Or you can use invokeAll to submit a collection of tasks. No real difference, just a matter of convenience to your coding situation.

    How to make main wait for threads to execute

    You can wait for an executor service to complete all its tasks, with the original thread blocked until then. To manage this block-to-wait-for-completion, you have two routes, a manual route and a more convenient automatic route:

    • To manually manage, see the boilerplate code given to you on the Javadoc for ExecutorService interface.
    • Since Java 19, an ExecutorService object is AutoCloseable. So we can use try-with-resources syntax to more conveniently wait for completion. (You may want to inspect the source code in OpenJDK to see if the behavior complies with your needs.)

    We use the more convenient try-with-resources approach in code below.

    but these threads must be started at the same time

    That is not possible. A thread in Java is backed by a native thread managed by the host OS. The scheduling of those OS threads is outside your control, and outside the control of the JVM.

    An executor service will attempt to immediately begin executing your tasks (unless you specified a delay with a ScheduledExecutorService). So an executor service backed by multiple threads on a multiple-core machine will likely begin executing multiple tasks at nearly the same moment. But maybe not. For example, if the machine were overburdened then some of your tasks may have to wait. Or the host OS may prioritize other threads before yours. You can generally expect your tasks to begin very soon, but not deterministically so.

    Example code

    package work.basil.example.threading;
    
    import java.time.Instant;
    import java.util.List;
    import java.util.concurrent.*;
    
    public class BunchOfTasks
    {
        public static void main ( String[] args )
        {
            // Define your tasks to be performed.
            List < Callable < Instant > > tasks =
                    List.of (
                            ( ) -> {
                                System.out.println ( "x" );
                                return Instant.now ( );
                            } ,
                            ( ) -> {
                                System.out.println ( "y" );
                                return Instant.now ( );
                            } ,
                            ( ) -> {
                                System.out.println ( "z" );
                                return Instant.now ( );
                            }
                    );
    
            // Submit your tasks. Capture `Future` objects. 
            List < Future < Instant > > futures = List.of ( );
            try (
                    ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor ( ) ;
            )
            {
                futures = executorService.invokeAll ( tasks );
            } catch ( InterruptedException e ) { throw new RuntimeException ( e ); }
    
            // Report results by inspecting `Future` objects.
            for ( Future < Instant > instantFuture : futures )
            {
                try { System.out.println ( instantFuture.get ( ).toString ( ) ); } catch ( InterruptedException | ExecutionException e ) { throw new RuntimeException ( e ); }
            }
        }
    }
    

    Example output:

    x
    y
    z
    2023-11-12T20:51:42.098634Z
    2023-11-12T20:51:42.098742Z
    2023-11-12T20:51:42.098795Z