Reputation: 340350
I use Thread.sleep
when experimenting or demonstrating Java code for concurrency. By sleeping, I am faking some processing work taking place that will take some time.
I wonder about doing this under Project Loom.
Thread.sleep
in the same way?To educate myself, I have watched some late 2020 videos with Ron Pressler of Oracle presenting Project Loom technology (here, here). While enlightening, I do not recall him addressing the issue of sleeping a thread.
Upvotes: 11
Views: 5002
Reputation: 340350
The Answer by John Bollinger and the Answer by Stephen C are both correct and informative. I thought I would add a code example to show:
Thread.sleep
.Let's simply write a loop. On each loop, we instantiate a Runnable
to perform a task, and submit that task to an executor service. Our task is: do some simple math, subtraction from the long
returned by System.nanoTime
. Finally, we print that number to the console.
But the trick is that before the calculation, we sleep the thread performing that task. Since each and every sleeping for an initial twelve seconds, we should see nothing appear on the console until after at least 12 seconds of dead time.
Then the submitted tasks perform their work.
We run this in two ways, by enabling/disabling a pair of commented-out lines.
ExecutorService executorService = Executors.newFixedThreadPool( 5 )
ExecutorService executorService = Executors.newVirtualThreadExecutor()
package work.basil.example;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TooFast
{
public static void main ( String[] args )
{
TooFast app = new TooFast();
app.demo();
}
private void demo ( )
{
System.out.println( "INFO - starting `demo`. " + Instant.now() );
long start = System.nanoTime();
try (
// 5 of 6 real cores, no hyper-threading.
ExecutorService executorService = Executors.newFixedThreadPool( 5 ) ;
//ExecutorService executorService = Executors.newVirtualThreadExecutor() ;
)
{
Duration sleep = Duration.ofSeconds( 12 );
int limit = 100;
for ( int i = 0 ; i < limit ; i++ )
{
executorService.submit(
new Runnable()
{
@Override
public void run ( )
{
try {Thread.sleep( sleep );} catch ( InterruptedException e ) {e.printStackTrace();}
long x = ( System.nanoTime() - 42 );
System.out.println( "x = " + x );
}
}
);
}
}
// With Project Loom, the flow-of-control blocks here until all submitted tasks have finished.
Duration demoElapsed = Duration.ofNanos( System.nanoTime() - start );
System.out.println( "INFO - demo took " + demoElapsed + " ending at " + Instant.now() );
}
}
The results are startling.
Firstly, in both cases we see a delay of just over 12 seconds before any console activity. So we know that the Thread.sleep
is being truly executed by both platform/kernel threads and virtual threads.
Secondly, the virtual threads complete all the tasks in mere seconds versus minutes, hours or days for the conventional threads.
With 100 tasks:
With 1,000 tasks:
With 1,000,000 tasks:
With conventional threads, we can see a repeated burst of several lines suddenly appearing on the console. So we can see how the platform/kernel threads are actually on the core, blocked, as they wait for their 12-second Thread.sleep
to expire. Then all five threads wake up at about the same moment, having all started at about the same moment, every 12 seconds, simultaneously do their math and write to console. This behavior is confirmed as we see little usage of the CPU cores in the Activity Monitor app.
As an aside: I would assume the host OS’s notices our Java threads are actually busy doing nothing, and then using its CPU scheduler to suspend our Java threads while blocked, to let other processes such as other apps use the CPU cores. But if so, this is transparent to our JVM. From the JVM’s perspective, the sleeping Java threads are taking up the CPU during the entire nap.
With virtual threads, we see dramatically different behavior. Project Loom is designed such that when a virtual thread blocks, the JVM moves that virtual thread off the platform/kernel thread, and puts in its place another virtual thread. This within-JVM swapping of threads is vastly cheaper than is swapping platform/kernel threads. The platform/kernel thread carrying those various virtual threads can stay busy rather than waiting for each block to pass.
For more info, see any of the recent (late 2020) talks by Ron Pressler of Project Loom at Oracle, and his 2020-05 paper, State of Loom. This behavior of rapidly swapping blocked virtual threads is so efficient that the CPU can be kept busy the entire time. We can confirm this effect in the Activity Monitor app. Here is a screenshot of Activity Monitor running the million tasks with virtual threads. Notice how the CPU cores are virtually 100% busy after all million threads finish napping for 12 seconds.
So all the work is effectively being done immediately as all million threads were simultaneously taking their 12 second nap while the platform/kernel threads were taking their naps serially in groups of five. We see in that screenshot above how the work of the million tasks is being done all at once in a matter of seconds while platform/kernel threads do the same amount of work, but spread it out over days.
Note that this kind of dramatic performance increase only occurs when your tasks are often blocked. If using CPU-bound tasks, such as video-encoding, then you should use platform/kernel threads rather than virtual threads. Most business apps see much blocking, such as waiting for calls to the file system, database, other external services, or the network to access remote services. Virtual threads shine in that kind of often-blocked workload.
Upvotes: 7
Reputation: 719739
Looking at the source code, when you call sleep(...)
on a virtual thread, it is handled by the JVM's virtual thread scheduler; i.e. without directly doing a syscall and without blocking a native thread.
So:
Under Project Loom technology with virtual threads (fibers), can we use
Thread.sleep
in the same way?
Yes.
Is there any thing different or noteworthy about sleeping a virtual thread versus sleeping a platform/kernel thread?
Sleeping a virtual thread is handled like you would expect a virtual thread to behave. The performance will be different to a kernel thread, but the behavior is designed to be transparent application code ... that is not making unwarranted assumptions about thread scheduler behavior.
At any rate, the javadocs for Thread.sleep(...)
in Loom currently do not mention any differences between kernel and virtual threads.
Upvotes: 4
Reputation: 181932
- Under Project Loom technology with virtual threads (fibers), can we use Thread.sleep in the same way?
It appears so. I refer to the page on the OpenJDK wiki that addresses blocking operations in Loom. It lists Thread.sleep()
among operations that are friendly to virtual threads, by which it means that
When not pinned, they will release the underlying carrier thread to do other work when the operation blocks.
You go on to ask,
- Is there any thing different or noteworthy about sleeping a virtual thread versus sleeping a platform/kernel thread?
Documentation is sparse, and it is unclear whether any differences that may actually exist are intentional. Nevertheless, I'm inclined to think that the objective is for sleeping a virtual thread to have semantics as close as possible to those of sleeping an ordinary thread. I suspect that there will be ways for a clever enough program to distinguish, but if there were any differences that rose to the level of "noteworthy" then I expect they would be considered bugs. I base this in part on inference, but I also refer you to the State of Loom document over at java.net, which lists among its "key takeaways" that
- A virtual thread is a Thread — in code, at runtime, in the debugger and in the profiler.
and
- No language changes are needed.
(Emphasis added.)
Upvotes: 12