
import java.util.Random;

// this example is both concurrent AND paralle
// this means we express the algorithm in chunks that can
// overlap their executions in time
//
// we also then manage those chunks (threads) is such a way
// that when executing, they do make use of the multiple cores/CPUs
// our handware has available, and so we see parallelism... speedup
// in overall excution tims (at least until we saturate the cores
// available)
//
// this is managed by creating and starting many threads so they
// are all executing, and then synchronizing main to wait
// for all of them to end before more main code is executed

public class ParaDemo {
    public static void main(String[] args) {
        // System.out.println(Runtime.getRuntime().availableProcessors());

        for (int num_threads = 1; num_threads <= 24; num_threads++) {
            int num_items = 1000000000;

            Thread[] workers = new Thread[num_threads];

            long start = System.nanoTime();
            for (int i = 0; i < num_threads; i++) {
                int num_per_thread = num_items / num_threads;
                workers[i] = new Thread(() -> {
                    int num_to_do = num_per_thread;
                    Random rnd = new Random();
                    for (int j = 0; j < num_to_do; j++) {
                        rnd.nextDouble();
                    }
                });
                workers[i].start();
     
            }  // here all threads are made and started running


            for (Thread w : workers) {
                try {
                    w.join();  // wait for them all to end
                } catch (InterruptedException e) { }
            }

            long end = System.nanoTime();

            System.out.println("Overall elapsed with " + num_threads + " threads: " + ((end - start) / 1e9) + " seconds");
        }
    }
}
