Search code examples
javamultithreadingjava-threads

Unable to understand the time consumption by Java threads


I created a 1000 threads and each thread takes about 10 seconds to finish. During these 10 seconds the thread doesn't sleep but does a simple math operation. However, the program still finishes under 20 seconds on my PC (Intel i7 6 cores). I don't understand how. I was expecting it will take at least about 1000/6=167 seconds. Any idea what is going on ?

public class test
{
   public static void main(String args[]) throws Exception
   {
       long startTime = System.currentTimeMillis();
       Thread[] threads = new Thread[1000];
       for(int i = 0; i<1000; i++){
           threads[i] = new Thread(()->doSomething(10));
           threads[i].start();
       }
       for(Thread t : threads){ 
           t.join(); 
       }
       long endTime = System.currentTimeMillis();
       System.out.println("Time taken "+(endTime - startTime)/1000);
   }
   
   public static int doSomething(int seconds){
    long st = System.currentTimeMillis();
    long usageTimeInMillis = seconds*1000L;
    long startTime = System.currentTimeMillis();
    int i = 0;
    while ((System.currentTimeMillis() - startTime) < usageTimeInMillis) { i++; } 
    long lt = System.currentTimeMillis();
    System.out.println("Done "+Thread.currentThread().getId()+" in "+(lt-st)/1000+" seconds ");
    return i;
   }
}

Here is the partial output:

Done 48 in 10 seconds 
Done 36 in 10 seconds 
Done 597 in 10 seconds 
...
Done 206 in 10 seconds 
Done 217 in 10 seconds 
....
Done 462 in 10 seconds 
Time taken 17

The output shows that every thread does run for about 10 seconds. Then how come a 1000 threads finished in 17 seconds?


Solution

  • As commented by Maurice Perry, you devised a task that would take ten seconds to execute, but that is ten seconds of time elapsed on the clock, not ten seconds of CPU activity.

    You loop repeatedly, checking the current time on each loop. You check the time by calling System.currentTimeMillis(). Your intention was to keep the CPU core busy for the entire ten seconds. But that is not what is happening.

    The host OS managing the platform threads in Java schedules a thread for execution at a time, and for a duration, of its own choosing. Your Java platform thread may be paused at any time by that host OS. So, in an extreme example case, your code may be checking the current time once, for a result of 2024-01-23T00:00:22.123Z. Then the host OS may suspend your thread. Your thread does nothing for several seconds. Eventually the host OS schedules that thread for further execution. The second time your code checks the time, the result is 2024-01-23T00:00:34.567Z. So over 12 seconds elapsed, but your code ran only briefly, just checking the current time twice.

    That example is a bit extreme, as the host OS will not generally let a thread go 12 seconds without executing. But if your host computer’s CPU is severely overburdened, such as running a thousand Java threads, such long thread suspensions could indeed happen.

    So the behavior you experienced is a feature, not a bug. A thousand tasks waiting for ten seconds to expire on the system clock will indeed, in total, take about ten seconds.

    Running your exact code takes 20 seconds on my MacBook Pro, 16-inch, 2021, Apple M1 Pro, 10 cores (8 performance and 2 efficiency), 16 GB RAM, macOS Sonoma 14.3.1, Java 21.0.1 in IntelliJ IntelliJ IDEA 2023.3.4 (Ultimate Edition).

    Then I modified your code. I show the number of loops executed in each task.

    package work.basil.example.threading;
    
    
    import java.time.*;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.stream.IntStream;
    
    public class BusyBusy
    {
        public static void main ( String args[] ) throws Exception
        {
            System.out.println ( "Demo start at " + Instant.now ( ) );
            final long demoStartNanos = System.nanoTime ( );
    
            final int countTasks = 1_000;
            try (
                    ExecutorService executorService = Executors.newFixedThreadPool ( countTasks ) ; // We want one task per thread. So one thousand tasks means one thousand threads.
            )
            {
                IntStream
                        .rangeClosed ( 1 , countTasks )
                        .mapToObj ( TenSecondTask :: new )
                        .forEach ( executorService :: submit );
            }
    
            Duration demoDuration = Duration.ofNanos ( System.nanoTime ( ) - demoStartNanos );
            System.out.println ( "Demo end at " + Instant.now ( ) + ". demoDuration = " + demoDuration );
        }
    }
    
    class TenSecondTask implements Runnable
    {
        private final int sequenceNumber;
    
        public TenSecondTask ( final int sequenceNumber )
        {
            this.sequenceNumber = sequenceNumber;
        }
    
        @Override
        public void run ( )
        {
            final long tenSecondsInNanos = Duration.ofSeconds ( 10 ).toNanos ( );
            final long taskStartNanos = System.nanoTime ( );
            long countLoops = 0;
            while (
                    ( System.nanoTime ( ) - taskStartNanos ) < tenSecondsInNanos
            )
            {
                countLoops++;
            }
            System.out.println ( "Task # " + this.sequenceNumber + " in thread ID " + Thread.currentThread ( ).threadId ( ) + " done at " + Instant.now ( ) + ". Loops: " + String.format ( "%,d" , countLoops ) );
        }
    }
    

    My results showing a wide range of over a hundred thousand to over seven million. Total time elapsed is about 20 seconds, same as your code.

    Demo start at 2024-02-15T09:24:13.773544Z
    Task # 6 in thread ID 26 done at 2024-02-15T09:24:23.779903Z. Loops: 328,186
    Task # 11 in thread ID 31 done at 2024-02-15T09:24:23.812401Z. Loops: 349,456
    Task # 13 in thread ID 33 done at 2024-02-15T09:24:23.839549Z. Loops: 356,229
    Task # 20 in thread ID 40 done at 2024-02-15T09:24:23.883941Z. Loops: 379,757
    …
    Task # 991 in thread ID 1019 done at 2024-02-15T09:24:33.561808Z. Loops: 981,788
    Task # 992 in thread ID 1020 done at 2024-02-15T09:24:33.566229Z. Loops: 1,034,678
    Task # 993 in thread ID 1021 done at 2024-02-15T09:24:33.572045Z. Loops: 1,172,157
    Task # 994 in thread ID 1022 done at 2024-02-15T09:24:33.576395Z. Loops: 1,067,262
    Task # 995 in thread ID 1023 done at 2024-02-15T09:24:33.582312Z. Loops: 1,034,725
    Task # 996 in thread ID 1024 done at 2024-02-15T09:24:33.586528Z. Loops: 1,134,316
    Task # 997 in thread ID 1025 done at 2024-02-15T09:24:33.592593Z. Loops: 1,112,738
    Task # 998 in thread ID 1026 done at 2024-02-15T09:24:34.158249Z. Loops: 6,455,185
    Task # 999 in thread ID 1027 done at 2024-02-15T09:24:34.158381Z. Loops: 6,484,593
    Task # 1000 in thread ID 1028 done at 2024-02-15T09:24:34.168723Z. Loops: 7,411,728
    Demo end at 2024-02-15T09:24:34.168958Z. demoDuration = PT20.392301916S
    

    By the way, System.currentTimeMillis() was years ago supplanted by Instant.now(). Calling Instant.now captures the current moment with a resolution of microseconds rather than mere milliseconds in Java 9+ on implementations based on OpenJDK code base.