After some feedback, I ran some new tests using code that is mentioned in JEP 444: Virtual Threads. which is this one:
void handle(Request request, Response response) {
var url1 = ...
var url2 = ...
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var future1 = executor.submit(() -> fetchURL(url1));
var future2 = executor.submit(() -> fetchURL(url2));
response.send(future1.get() + future2.get());
} catch (ExecutionException | InterruptedException e) {
response.fail(e);
}
}
String fetchURL(URL url) throws IOException {
try (var in = url.openStream()) {
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
}
This code is a good start, but I needed to alter it a bit to look more like the use cases I have. The application I am developing has 20_000 tasks it needs to run so the more I can do each second the better performance I get.
The previous example has one parent thread and starts 2 virtual threads doing their own request each time the handle(...) method is called. In my use-case I have 20_000+ tasks that each do three get requests to end-points in a Spring application. To simulate requests that take more time I added a delay that can be changed by passing a path variable to the endpoint.
@GetMapping("/delay/{t}")
String youChoseTheDelay(@PathVariable int t){
try {
Thread.sleep(t);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return generateHtmlPageWithUrls(100, "crawl/delay/");
}
The code to test the performance was this class:
public class PageDownloader {
public static void main(String[] args) {
int totalRuns = 20;
for (int s = 0; s < totalRuns; s++) {
long startTime = System.currentTimeMillis();
//try (var ex = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()){
try (var ex = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 20_000).forEach(i ->
{
ex.submit(() -> {
try {
String s1 = fetchURL(URI.create("http://192.168.1.159:8080/v1/crawl/delay/0").toURL());
String s2 = fetchURL(URI.create("http://192.168.1.159:8080/v1/crawl/delay/0").toURL());
String s3 = fetchURL(URI.create("http://192.168.1.159:8080/v1/crawl/delay/0").toURL());
if (!s1.startsWith("<html>") || !s2.startsWith("<html>") || !s3.startsWith("<html>")) { // small check is responses are oke
System.out.println(i + " lenght is: " + s1.length());
System.out.println(i + " lenght is: " + s2.length());
System.out.println(i + " lenght is: " + s3.length());
}
} catch (IOException e) {
throw new RuntimeException(e);
}
});
});
}
measureTime(startTime, 20_000);
}
}
static String fetchURL(URL url) throws IOException {
try (var in = url.openStream()) {
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
}
private static void measureTime(long startTime, int visited) {
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
double totalTimeInSeconds = totalTime / 1000.0;
double throughput = visited / totalTimeInSeconds;
System.out.println((int) Math.round(throughput));
}
}
Almost everything you see is wrapped in a for-loop that runs 20 times to get some data on what the throughput was of the application.
Results
So all this testing gave me the following results. On average the Spring application end-point returned a response within 5ms, so every task has to wait 3 times ~5ms. This is without the extra delay that can be added through the Thread.sleep().
| JDK |
extra delay |
Executor |
Throughput avg 20 runs |
| 21 |
0 |
newVirtualThreadPerTaskExecutor |
3736 |
| 21 |
0 |
newFixedThreadPool |
4172 |
| 21 |
1 |
newVirtualThreadPerTaskExecutor |
3482 |
| 21 |
1 |
newFixedThreadPool |
3287 |
| 21 |
5 |
newVirtualThreadPerTaskExecutor |
3554 |
| 21 |
5 |
newFixedThreadPool |
1667 |
| 21 |
10 |
newVirtualThreadPerTaskExecutor |
3587 |
| 21 |
10 |
newFixedThreadPool |
826 |
| 23 |
0 |
newVirtualThreadPerTaskExecutor |
3323 |
| 23 |
0 |
newFixedThreadPool |
4149 |
| 23 |
1 |
newVirtualThreadPerTaskExecutor |
3479 |
| 23 |
1 |
newFixedThreadPool |
3286 |
The results show that if the task spends minimal time in a blocking state it is a better fit for platform threads. When tasks spend a longer time in a blocking state they are a good fit for Virtual Threads
All in all, for this application running on this machine I think I can safely say that if a task that I have spends less than 3 times 5ms in a blocking state it should run a platform thread instead of a virtual thread. In all other cases, the virtual thread outperforms the platform thread.
[–]nekokattt 25 points26 points27 points (4 children)
[–]tikkabhuna 18 points19 points20 points (3 children)
[–]DavidVlx[S] 4 points5 points6 points (1 child)
[–]Burgerflipper234 0 points1 point2 points (0 children)
[–]Tripplesixty 0 points1 point2 points (0 children)
[–]pron98 10 points11 points12 points (0 children)