jPrime 2026

Virtual Threads Are Not Async I/O: Lessons from Operating System Archaeology, Ivan Yonkov

50:54 · 03 Jun 2026 – 04 Jun 2026 · YouTube

About this talk

This talk by Ivan Yonkov explores the complexities of virtual threads in Java, particularly emphasizing that they are not asynchronous I/O. He presents an in-depth analysis of input and output operations, detailing which are blocking and which are non-blocking, referencing various operating systems like Linux and Windows. The speaker discusses the inherent challenges of file I/O compared to sockets and demonstrates how these differences impact performance when using virtual threads. Throughout the session, he shares historical insights and practical examples, underscoring the limitations of current Java implementations and the impact of operating system internals on asynchronous behavior. The talk aims to clarify misconceptions about virtual threads and their capabilities in managing I/O operations.

Full transcript

Okay, everybody. Please have a seat. I know it was a long break. You should be filled with caffeine and beer in combination. And if you have this combination in your blood, you will for sure understand what this I would call the sexiest speaker of JPrime 2026. Big applause. Yes, officially the sexiest speaker of our conference. Yes, and he will talk about extremely sexy topics, which are the

virtual threads. most probably he have hit a lot of stones under water stones with those virtual threads. finally, he said something completely obvious. Virtual threads are not a sync IO. >> [laughter] >> No No absolutely. So and I wonder if he has 50 minutes to talk about that virtual threads are not a But I'm pretty much sure he will do it in style. Big applause. >> Thank

you. Thank you. Thank you for having me. Mitya is correct. I'm here as a Captain Obvious just to tell you the obvious thing. The virtual threads are not a sync IO. And probably half of you already know it. Half of you maybe And even those of you who know it, maybe you will understand a little bit better why it's not in a sync IO. Uh and uh

this talk is actually ingested as an archaeology from the operating systems uh um lessons that we can take uh for the past 30 years uh from Linux, Windows, Mac OS, and so on and so forth. And actually this is a disguise uh in most of the time we will not talk about You had plenty of them last year on J Prime, before that on J Prime, and

in every conference all over the world once the virtual threads were released, everyone was talking about them. Now it's AI. So I decided to take a step back and talk about virtual threads, but disguise the talk with virtual threads because you may come, and then I will actually talk about different things. Before that, if you have any questions, remarks, comments, whatever you want, you can scan the

QR or use the short link here. It will open a slide. Though it's not a virus. Hopefully. Trust me. And you can uh you can put your questions there. I will open them on my phone in the end of the session. Or if it's something interesting, someone just raise your hand. I will open it in mid-session. I will not show it on the screen because I know

you. This QR code will stay uh on the top left for the next two slides and then it will disappear. So if you are actually want to contribute with some question, comment, or something like that, you have a little bit more time to scan it. Uh and actually we were going to talk about uh the input output operations under the hood. Which of them are blocking, which

of them are non-blocking by default, and how does it does it correlate with what we are going to talk about in Java. And one obvious thing is the file systems because they're the fundamental part of one of the most common IO operations that we are doing and it is reading and writing to files. I have an interesting case study that I cannot show you here, but if

you are interested I will I will tell you after what we what we had in one of the one of the projects. And of course this pretty much correlates to the Oops. operating system internals. Because it may differ in what happens in an operating system family of Unix and what it happens in operating systems in Windows for example. And even the Windows family throughout the years had

some differences. So, it's an evolutionary thing even in the Linux operating system and we will cover some of them. In the abstract of the talk it was advertised that we will take some lessons from the Windows operating system and we will for surely do, but most of the time we will speak about and the Unix family because this is where you most probably deploy your applications. And

if you are unlucky to deploy on Windows, then you have something to see there. We were unlucky to do the other part to migrate a Java application that's more than 20 years deployed in Windows Server 2003 and then on Linux operating system, it's a nightmare. Uh so, who am I? Well, uh my name is Ivan Yonkov. Obviously, you saw it. uh I'm this guy that's actually doing

a lot of stupid things. For example, you know, uh Mihaita said the sexiest and the stylish uh speaker. Probably he said Probably he wanted to say someone stupid that's uh dressing uh um inappropriately and telling uh inappropriate things. That's me. Yeah, I give my jacket for for instance for to the the camera guy. Now I don't have jacket, so don't worry, not going to undress on the

stage now. what I do well, I write code to Java, so I don't write Java. That's uh very good for me, and I break people fingers. No, I'm not motor. And I do it only if people don't follow the solid principles. So, but this is not a part of this talk, so I'm not going to talk it. And uh usually I'm destined to maintain a legacy code

bases. If you are in the Java space, you usually do it. That's it, unfortunately. And uh that's why I call myself a Java archeologist. I'm tending to the new things, but what's my destiny? Java 6, Java 4, Java 8 at best, Windows Server 2003. That's it. last but not least for some reason I'm somehow still managing to have a company on my own, which even in the

AI era is still surviving somehow. But it doesn't matter also for that talk. in order to present every one of these things that we had in the agenda, we will start with an example. And uh this example will start with Java. I say I'm saying it will start with Java because we will not cover only Java now, even though we are a Java And uh in Java

there is a mo- one of the most primitive way to send an HTTP request. I know you're working with Spring, Quarkus, HTTP clients, all the decorative stuff, and so on and so forth, but in the end of the day you can do a new URL and send an HTTP request. And what happens when you do that? Well, uh you receive a response at some point. But this

is not what what we are talking about. We are talking about that there is an operating system that our Java program is communicating with or vice versa or the operating system calling the the Java program. And whenever a process starts, it opens two three file descriptors numbered from zero to two, which are for the input output in the error stream. And when we do a read operation

in an input output way, we usually create a new file descriptor. When we call the CPU, it says, "Okay, you receive another one at this point number three, which is the socket one." Why the socket one? Well, because the CPU stays on the motherboard. And at the other end of the motherboard, there is an ethernet adapter. And usually through a device driver, the CPU tells the ethernet

adapter, "Hey, call the other part of the world." And what you happens actually is you wait. You wait. You wait. You wait. And then the CPU is called with an interrupt. "Hey, I have something for you to a DMA and other kind of stuff in the bare metal. Something happens and the CPU is notified that the ethernet adapter actually did his job did their job. And this

file descriptor number three is actually notified with the things that are there in a shared memory space, probably a byte array is there which our program receives. And we receive the output that we saw earlier. In a sense, that's what is happening when we do that call, but the problem here is if that was a blocking call. We had this wait thing, even though that we know

that most of the IO operations can be non-blocking, right? And if we do second one, we need to open a new socket descriptor, and we need to wait for the first one to finish, then wait for the second one to finish. Even though these two socket connections are not dependent one of the other. They can do in parallel without any problem. And now you will say, "Okay,

and what we What if we do this?" Wrap any of these in a virtual thread. And this is very interesting thing. But before continuing, I want to show you an This is Well, this is not something that you see for some reason. Oh, probably I need to do that. And now you can see it. This is a example where you read a file. It's a 64 MB

binary file that we need just to read and say that we read we read it. So, if I run it, this one file, it takes 46 ms. Correct? And also I have run a slow server in Python that sleeps for half a second. And then I will walk One HTTP request, as you can see here. How much time is going to take? Well, it a little bit

of a transport latency and half a second of uh sleep that we have, correct? Woohoo! 500 ms. So, in this example, our HTTP request, one of them, 10 times slower than one read of a file, which was only 46 milliseconds. So, my question now is what will happen and running a new virtual thread per task executor all the 10,000 of sockets. 68 someone said? Let's see that.

It depends is the best answer here because you don't know uh how the threads uh in my CPU will be mm will be loaded. I'm now presenting and stuff and it takes a a little bit more time. But, it took around 9 seconds to finish. Uh it spawned a lot of uh virtual threads uh in in behind. They are bound to some carrier threads. They were waiting

for half a second the carrier thread was released so we were able to spawn a lot of threads. I'll go to the one file again and do it for instance 2,000. Let's not even compare it to 10,000. Let's do it with 2,000 which took around for one file for 50 milliseconds. How much is going to take for you? What do you think? It depends again? It depends

again. But, let's try it. It was 9 seconds for the socket example. Now, we wait. And we wait. And we wait a little bit longer. And again, we wait. And then, it finished. 14 seconds. 2,000 files. Each of them was performing 10 times better than the sockets, but when you scale for 2,000 it was sucking big time versus the sockets which were 10,000. Why? It was the

same code, right? It was new virtual thread per task executor and we were reading all the bytes of file. Well, this is what we are going to dissolve So, if you do that, it's going to work pretty good because we saw it, right? But, in order to understand what's inside, there are three things which are different but same the same fundamental. So, if you understand either the

new continuation API in Java, new like five versions of Java so far, or you understand something like a reactive runtime, right? Netty, or if you're a full stack developer, probably Node.js, or any other JavaScript runtime, or the concept of the event loop, which is actually behind Node.js and Netty and other runtimes that use the reactive pattern. I'm not going to explain that in detail even though I'm

going to explain a little bit, but you need to be you need to understand a little bit any of of these. Even one of of them would be good for you. And speaking of Node.js, let's imagine that this code is in a language that has promises like JavaScript. these things should also go in parallel, right? Why is this working in parallel with the promise based or whatever

is for instance in Java the the future based programming? Because of something that we call an event loop. And event loop is pretty simple. It says, "Okay, we need to wait for messages." What is wait for messages? Well, wait for messages is actually something that's calling the operating system. If there are unprocessed messages, the operating system tells us how we will understand. But if there are and

they are not processed, what we need to do? We need to sleep. At some point when one of them, at least one of them is good for processing, we need to return true so the while loop will actually loop. Or if there are no messages and we need to stop our program, it should return false. It's simple. But how the hell this is actually working in the

operating system level? And since you are uh deploying on a Unix operating system, probably, I'm going to start with that. These two socket descriptors, three and four, assigned to the HTTP requests that we have, also have some memory that's given to the internet adapter so it knows how to tell the CPU it's an interrupt, but it's an interrupt for the descriptor four, for instance. You can send

two HTTP requests and the second one to finish first, right? This is one of the fundamental things of a parallel programming where things in parallel can finish either way they need. Then it can say, "Okay, interrupt the descriptor three." And if we imagine this is a pseudo code in Java, it's like, "Okay, let's go throughout all the descriptors and if any of them is ready, process it."

Or let's say this is a shared memory, so the third descriptor, the third file descriptor, is the memory address space hexadecimal cafe and the fourth one is the hexadecimal babe. So if we imagine this in descrip- I I not in descriptors, but in uh other space and um mem- memory spaces, this is uh going to look something like this. Except that the Java process cannot do that.

The Java process cannot own the memory space of these socket descriptors. And if it were ever possible to write that code, it's not going to succeed. It's going to do something like that. Well, uh it's still we're still not in So, probably it's going to do something called a segmentation fault, but it's still something that Java cannot do. Java cannot do a segmentation fault because you cannot

touch the internals operating system and the CPU. And you will say, "Okay, Java cannot do it. Fine. assembly language can do it, right?" Who speaks the CPU language? Assembly language. And you can say, "Okay, let me write in assembly the out the the out instruction." No. If you run this in an operating system user space, this is not going to work. This is a ring zero privileged

instruction, which even though which even if you open Windows, Linux, whatever, Mac OS, take this assembly, link it and build it and do it whatever and invoke it, it's not going to the only process that's flocked as a privileged process is your operating system shell. And everything else run as a subsequent process is flocked in a user space ring and you cannot do it. For this specific

instruction, actually, you can ask for permission. How do you ask in permission in assembly language? You need to call something that's called IOPerm. IOPerm is something that's called a system call. And it's numbered with some number, and you need to invoke in assembly sys call to do I/O perm. And we are going to actually talk fundamentally in this in this talk. If you imagine this in a

C code, uh it's uh going to look like something You call I/O perm, then you call I/O B. I/O perm is a C code wrapper over a system call that the Unix operating system provides us. But let's expand the example to something more familiar to you. Let's open two files or open a file. To open a file, uh we need two things. One of the thing do

the open, and the other thing is do the read. So, how do we do the read? We need first to invoke the open system call call, which is the number two. Then we need to move into a register the file descriptor that's uh returned by the system call. And then we need to call the system call read. Read uh is also a system call, and you can

see that uh both of them are numbered. One of One of them is numbered two, and the other is numbered zero. How do we know all of these uh numbers? Well, there is a table, for instance, for the 64-bit uh processors. You can open x64.syscall.sh and see hundreds of them. For instance, these ones. And as you can see here, is read, and two is open. And if

we apply this code to these two things, we can see exactly that in the EAX register, we are putting these two numbers. First, the open with number two, and the other read with the number zero. If we move the page on the 200 and something, we will see two other system calls which are very fundamental to what we're going to talk about now. And these are the

Apple family system calls. E pole is uh the de facto standard mechanism that the Unix operating system and the subsequent languages that are providing abstractions over Unix operating system are using to do I/O operations. And uh this is uh working entirely in the user's user space perspective. So, the you as a programmer can invoke this system call. And the idea is that you have to list the

interests in the ready list. You're interested in some descriptors to be uh ready. And whenever they are ready, the operating system populates them or the so-called kernel. So, the Unix kernel, whenever you open a socket, can tell you, "Hey, there is a there is something that's ready for you and you can do it." To do it in a non-blocking way, so you don't want to block your

sockets like we in the beginning, we need to tweak our open calls. So, the open system call has uh some options that you can pass. And the options that you can pass, some of them are these two, option async and option non-block. You can do that for sockets. You can do that for terminals, serial terminals, for sockets, and even for pipes and FIFOs. What is not listed

here? Files. You cannot do that for files. So, you You open files in a non- in a sync way. Can you do that for for a non-blocking way? Well, no. This flag has no effect for regular And if you open a file descriptor that's a file base and you pass it to epoll, you will receive an error. So, files through epoll you don't enter this event loop

that we saw because epoll is the fundament of uh uh of the event loop. Uh I can show you a little example with the epoll event loop without files because as we saw there, we cannot do that. Uh and how I will show that? Well, you say okay, uh we were in uh in macOS. We cannot do that in macOS but because if I do something like

you name, you see I'm on Darwin operating system but I can go in Linux. And uh I have an example in my demo where I will remove what I already did and will show you something which is is I think epoll C code. I will not go into details, of course. The idea is that what we need to do is open several operations that can benefit from

the non-blocking flag. For instance, an anonymous pipe. You saw in the documentation, pipes are fine. You saw them? And what you do? You open a pipe, you receive the descriptor, then you set non-blocking. What is uh set non-blocking? Well, set non-blocking is just flag that says this option O underscore non-block that you already saw. Just say just do it later, not on the on the time you

do open, but it's fine to do it then. As you can see, what is marked here, we do send set non-blocking for a timer, we do it for anonymous pipe, and we do it for a named pipe. All these three things we can put in as a task in an event loop, and then we start looping. And whenever the operating system on the wait level here says,

"Okay, we have some thing that's ready because this is actually going to sleep if nothing is ready. But if anything is ready, you will receive it, and you see that it's either a pipe, you see it's either a timer, you receive if it's something else, but we are now supporting in our minimal example pipes and timers. So, how we are going to run this? Well, one of

the pipes, if I go again, sorry, is anonymous one, and we are doing it from the same process. So, we open a and every five ticks here, every five ticks, we are writing to the pipe. From the same process, we are writing, and when we write to a pipe, we will go here. Print. So, on the next iteration of the loop, you will see that. But there

is also a pipe that's named, and we can write it to it from another process. So, I need to compile two things. One is the a sync e poll, and the other is push to pipe. What is actually push to pipe doing? Push to pipe is opening the same pipe that's shared from both of the processes, and whenever I type something on the keyboard, it's going to

go there. So, I will actually do something which is splitting my screen, not dev, but demo, and compiling the named pipe. So, what I'm going to do is do something like that, And then I'm going to call it. It's doing nothing now. I need to open it for read as well. So, I will compile which one? The sinky pole. A sinky pole. And then I will open

it. As you can see in the sinky pole, it's printing each second timer ticks and to each 50 iteration, it's also pushing to the anonymous pipe and you see it from the anonymous pipe. But if I go here and say hello JPrime, you can see it directly in the other So, it's reacting in an asynchronous fashion. You don't need to wait for anything. And to receive it,

I can say And you can again see it here. Read from pipe. So, this is a minimal implementation of an event loop that libraries like Netty or the Node.js runtime are doing behind the scenes. And they're doing this in in the Unix operating system pretty well, except that this thing is not supporting files. And once you do it for the file, you suck big time. So, uh

this is a minimal example of what epoll can do for you and what epoll cannot do for you. And since epoll cannot do something for you, we need to see what's happening and is this thing not working very well. but we have the neo which should be using something to do it. File.ReadAllBytes. Or new URL like we saw it before. These two things, both of them, are

going from the neo part. what's happening behind the scenes is in the top is using the period zero, which is the blocking one of reading. It's actually doing just read without the poll, without anything. You wait for a byte to receive to receive a byte and and the file system should give it either from the page cache or for from the disk adapter or somewhere where they

can fetch it. You cannot do that in an asynchronous level using this native period zero. On the other hand, we have the time to read from the URL socket, the socket input stream, and it's calling the park method, which is pretty much adapted to either net.poll, which will use whatever I show you, or it will be adapted to poller.poll if you are in a virtual thread context,

which will park your virtual thread, and it will be woken up once the interrupt comes. So, in this case, the virtual threads only work fine if you are if you have something in the operating system internal that is doing it. In a sense, things like sockets have two states, ready or not ready. Things like files, they all So, they only have always ready. So, if you have

ready not ready, you can poll the file descriptor, park the virtual thread, wake it up later, no worries. If you have always ready, you need to spawn one more carrier thread that listens for the interruption that something is ready to wake up your So, what's happening is that your virtual threads grow, but the carrier threads also grow. And at some point, you cannot grow more, and they

are cute. They stay in the queue, and you cannot spawn a new carrier thread, or even if you spawn it, your operating system cannot handle it. So, you need to wait the queue to be be empty, and then another carrier thread to spawn there. Even though your files are ready to be read, you cannot take whatever is read from them. And that was the problem in the

example we started with. And this is only if you go to the new path. If you go to the normal file input stream, it's even worse. In Java 21, they said, "Okay, we will do a broker begin, so we will compensate with one more carrier thread." But if you do But in Java 24, they changed it. You see now that they changed it, and this is because

the synchronized block in the methods before that were actually sticking the thread, and you have way more carrier threads than we wanted, so they needed to fix So, if you use the file input stream that read all bytes, it can act even You need to go to the new path. This one is not the new path. It's java/io, if you see, in the uh in the GitHub

uh in the GitHub killer. But on the other hand, they do it for the file channel implementation that sometimes they can say broker begin, and sometimes they they may not. It depends on the direct lock, and the direct lock is also something that the that the J the Java virtual machine says, "Okay, do I have this in the page cache? Do I have this is a big

file? How?" And it applies a strategy where it needs to compensate or not. If it's a pretty small file, it's not going to spawn a new carrier thread, and you will wait on that thread Uh so, they made it clever, but you need to know what you need to use inside your virtual threads. Otherwise, your virtual threads will not perform the way you expect. You will say,

"Okay, but now you can use an asynchronous file channel." Yes, you can. Asynchronous file channel will actually spawn on the future. The future, you can go get the thing in the future. And what will happen inside is it will go to a countdown which we're trying to acquire a lock. And once it acquires a lock, it will try try to park your thread. Saying whichever the blocker

is to be Well, if it's a virtual thread, it will use a bark virtual thread, which is the mechanism that we saw before that. But again, there is a problem. It's a file system which sets the ceiling. And if you try to see how get is correlated to the open command, to the open method, you can see that it says, "Okay, I need to fetch a provider.

Let's go to file system provider." And if you control click it, you'll see several providers. And on my macOS, and probably in Linux, it will use the third one, the Unix file system And the Unix file system provider, using the Unix channel factory behind the scenes, is using the open system call that you can through an open method. And this one will just spawn a new thread.

It will make a pool. This pool will probably be however cores you have. Let's say you have eight cores in your CPU, a executor service will spawn eight threads. These eight physical threads, OS level threads, will be the ones that will wake up your virtual threads. So, you still have the problem that you had before, even if you use the asynchronous file And now you will say,

"What happens then in Windows? You spoken about Windows, Windows, Windows. What happens in Windows?" It's worse. Let's see. Let's see if it's worse. Let me open a new terminal. I was in Linux. So, in Spanish when you say uno, what's the next number? Uno? Dos. This is my DOS 6.42, uh 6.22. I have my floppy disk inserted. The A drive. Which if I dir, I will see

the Win X Z. Let's run it. And see, because you say it's worse. Maybe it's worse, maybe it's not. Who knows? Anyone knows? Only one way to understand, right? Oh, it's booting a little bit of cute Energy Star motherboard and it spawns. Probably some of you know it, some of doesn't have didn't have the the work to work with Windows 98. But you all now need to

do it. Windows system calls big time than Unix system calls. They are not open, they are not read, they are not e-poll. These 230 system calls are completely different. Coming from the Win 32 header file that you import usually in C or C++ and you receive their level of system calls. And this is what a portable thing like Java should do. You and it calls either the

Unix or the Windows depending what kind of Java you have installed in your computer. And I have several examples and I will show you only one of them because we don't have a lot of time. But if you're interested, we can explore more examples. Visual Studio 97. Yeah, yeah, why not? Why not? What we are going to do here is read three files. How we are going

to read these three files is that Windows promised in 1994 that they will release something that's called overlapped IO. Overlapped IO is a way that you can put in a queue operations and whenever you put that handle in any read operation, this handle will act as a file descriptor and it will be notified back. This is what Windows team promised and this is in '94, so in

'98 in Windows '98 it should work. Three files here which we are going to read in a sync fashion and then we will see if they are complete. And if they are completed, we are going to write whatever is there. This is an event loop, the same as you saw in the poll fine, how is this read file sync is going to to work? Read file sync.

it is going to call the read file system call for Windows and one of the things that it's is going to say and send as a last parameter is the overlapped structure. Even if you open now read file in the Windows documentation in MSDN or whatever it's called currently, you will see that the last parameter is which you can pass and this is there in in Windows

'98. This is the overlapped A specific object called overlap that it comes exactly from Uh from this header here which is windows.h. And I'm going to run this And it's going to fail big time. I don't know if you see it, but it says file does not support overlapped. So they lied to us. In '94, they said we release it. they did not. Or at least it

does it how it seems. Because they said, "We're releasing this in '94, but in the NT family of Windows only." Which is the most famous Windows that's supporting NT family that you maybe used homes back then. Windows XP, hmm, yeah, yeah, Windows XP sounds like something that I'm going to use also. So, I'm on the A drive, which is my first floppy disk that I inserted in

this beautiful Mac. So, I inserted the second one, which is the B drive. And if you see it, you see Win NT. Win NT except It's going to read it, hopefully. Uh, again, again, again, this beautiful Energy Star. Woohoo, Energy Star. What is going to do What we're going to do now is test the exact same code that you've seen in Windows '98, but in Windows XP.

Here it is. Trying to on a beautiful hardware M5. Did they I pay this? Yes, I have I have a disk with this book and the license keys printed on the book, so yes, I did. Actually, my hardware seems to be slow, right? M5 and then you wait for Windows to boot for 1 minute. Again, this beautiful Visual Studio '97 with the exact same beautiful def folder.

Nah. my computer is at risk. Damn it. Damn it. They're going to put viruses Now I'll see probably Pamela Anderson or on my computer. What is it? The love letter. No, I'm not at risk. Please close it. And uh again, going to a full screen. See it? Is it the same? Is it not that full screen? It's not working. That's beautiful. Close well. Full screen? No, not

workspace. Full screen? Yeah. Okay. pretty familiar. Three files. These three files do the same thing. Read file sync. What is read file a sync doing? Well, it is doing the exact same thing. This is the actually same code. I just copy-pasted in both Windowses. Again, we do the overlapped structure here. So, and the files are there in the C drive as well. We can we can see

that because maybe you say, "Okay, maybe files are not there." Well, they should be there. Yeah, file one, file two, file three. They're here. Let's try to build it. Overlapped exit. Do I want to build it? Yes, I want to Ooh, works. Works. This says the first file. This says the next the second file. Even I can try even to I do it a little bit bigger.

Can I? Not cursor, but I think I want to increase the font. Yes, please. Please. Yeah, this is the first file. This is the second file. This is the third file. Well, the second file is longer. The first file is not not that long. So, I would expect that first file, then third file, then the second file is printed, but this is not how the overlapped uh

IO is working And they advanced. They they did uh on the top of our IO, another structure that's called the IOCP. And I'm not going to show you how IOCP will do with the exact same files, but we are going to do a little bit interesting thing. We will make web server that sleeps for 50 milliseconds before responding. The same with the Python, but I don't have

Python on the Windows XP, so I'm doing it again with C. try to run the socket, and then we are going to read a file and some hundred sockets to read as as well from this server. This server is sleeping, and what is it sending is a default response. Um it's just saying 200 response with uh something uh not relevant. So, we are going here to IOCP.

And uh we can explore a little bit uh what's uh happening here. We will go to the local host on port 8080, and this we're going to read this long file. It's a very long file. Uh not very very very long, but it's long enough to take more for the operating system to read it than the sleeps on the socket they're going. So, if you see the

main, you see that we first init the file, init the socket, then we do a while loop, we check the completion status, and then we check whatever of these is first ready. And whichever is ready first, we will print it on the screen. If it was the file, we will print the file. If it is the socket, we will print the socket. So, let's see if working

the way I do expect. Because the wife demos, you know. When you plug the HDMI adapter, it's But you can see a lot of uh a lot of HTTP requests, which are saying okay okay okay okay okay okay. There's some hundreds of them, and then the file is there. Which is a little bit of a longer file with nonsense with the war war um ipsum and other

things that probably Bulgarian people will understand if they see what's uh what's there. If we make the socket sleep a little bit more. 50 500 milliseconds. And we we go to the IOCP. You can imagine how people were programming on this IDE, right? Okay, IOCP. And then we printed the file and then the sockets. So, this is the exact same event loop that you saw with epoll,

but with files. And files are supported here. Is it Are you still on the uh opinion that Windows is worse? This is the worst operating system that I ever used. Ex- especially Windows XP, I can relate to you, but in terms of uh file systems and in terms of async operations, it's was actually working for '94. For '94, also not for '94 because Java is not from

'94, but at some point from Java, probably '98 or '96 or something like that, they support the Windows channel factory. And the Windows Channel Factory, contrary to the Unix file system, is using again an open command, but saying fox.overlapped equals to true, which you'll see overlapped IO, And also this open method, what is it doing behind the scenes? Well, it's using the new IOCP and then the

Windows asynchronous IO channel implementation using the IOCP behind the scenes. So, if you run on Windows, it will act very different. And yes, thousands, hundred thousands of files in Windows Server 2003 with Tomcat deployed as a service there were acting way better than in Linux. >> No matter if you put virtual threads, whatever threads you put, it's the operating system that sets the And you say Linux

cannot be that bad, right? And you'll be correct. 2019, 7 years ago, 7 years ago in the Unix kernel, IO Uring appeared. And IO Uring, what is it Whatever you call IO request, which is also file read, without blocking, can be applied there. And even an example is that like reading a file. So, you can do that in Linux. Not with epoll, you can do it with

IO Uring. The problem, Java doesn't support it. There are different libraries, even Netty as a transport protocol can use IO Still not for file readings. Problem. But they can other Indie developers also have some methods to do it with these native calls. You see like period zero for files with a native call to a proxy TC code. You can also do the IO ring. Some people released

libraries for it. I know no one to use them. Several years ago, Java said we have another way to do native calls. Project Panama. And this is the problem because everyone said, "Okay, now we need to do everything that we wanted to implement with IO ring in Project Panama. So, we will just stay with for a little bit longer and once we are ready with Project Panama,

we will go there." And you know, several versions of Java are still released and no nothing supports IO ring. So, hopefully we will. Otherwise, we are stuck with the problem that I have shown you. we need to recap that. So, async is not the one thing. The problem with async is that it needs two states. One of them is completed, but the other one is ready readiness.

And files in a traditional way don't have the readiness state. They're always ready and that's a problem. not every IO acts the same. Sockets differ from pipes and they differ from files. And files are the worst here because file system page caching, all the blocking things that are happening inside then. And virtual threads, synchronous model over over an asynchronous machinery. But, the synchronous machinery needs to be

there. A lot of people nowadays are using virtual threads like they will do async IO behind the scenes. They will not. If the code behind the scenes is not async based, it will spawn carrier threads or even or even it will block and it will release the thread after the blocking operation happens. Then you will need to compensate with another carrier thread. So, they are not silver

bullet and people are treating them like this. This is why I I wanted to make that talk for people that are just putting everything inside the cold box of a sync virtual threads because they think that magically it will happen a sync. If it's not a sync, it's never going to be a sync with virtual threads. And what will happen is you walk, you spawn new threads

that needs to act as a schedulers and at some point in a large scale they're going to fail and you have much worse performance than you had. So, operating system sets the ceiling. You can have epoll or kqueue on macOS, which has the ceiling, the problem with the files. You can have IOCP, which do not have it. IO_uring, which is also not having it. you know, whatever

happens inside your operating system is what your ceiling is and you need to know it, otherwise you're going to do a crappy Thank you very much.

From event

jPrime 2026

03 Jun 2026 – 04 Jun 2026

All event videos
Back to Watch