Conceptually, these are all variations of the same thing. The difference is in how you use them and how much boiler plate you need to use them.
Funnily enough, the first versions of Java did not support OS threads and only had green threads for a while. Supporting real threads was a big deal at the time as it allowed you to use more than 1 processor. Of course, processors were single core at the time and most computers only had one of those anyway. Java 1.1 laid the foundations for proper multi threading and green thread support was eventually removed with Java 1.3. With Java 1.5 we got the java.concurrent package which enabled doing more complicated things with locks and other synchronization primitives that were a bit less primitive & brittle than using the synchronized & volatile keywords. That includes implementing green threads on top of real threads. Which is what frameworks like vert.x and others have been doing for ages.
So, in a way we're coming full circle here with virtual threads re-using the thread API, which in turn reused the original green thread APIs in Java 1.0.
That leaves off basically the whole point of it: JVM-native blocking calls don’t have to actually block, they can do an OS-native async call, and do some other work on the thread, returning upon completion.
Since Java uses very little FFI, it will benefit greatly from this automagical “no more blockingness”, of course only when there is some other available work in the meanwhile. Servers are the best fit for that.
I guess the idea is that you don't have to write tasks explicitly - if you have a sequence of actions you can just write it as a regular code, and the runtime will automatically insert points where your code might be suspended waiting for IO or other threads.
Don't be confused by the example, which semantically just shows a thread pool. It is hard to give an example in code as virtual threads ideally are purely an implementation detail and otherwise just look like normal threads.
In a typical thread pool if you schedule more long running tasks than you have worker threads, you either have to queue your tasks waiting for a free worker or you have to spawn a new worker. If your tasks are CPU bound and you do not care about latency/fairness, queuing is what you want. If your tasks might do blocking operations, you might underuse your CPU, or you simply want more fairness and not simple FIFO execution; in this case you can spawn new threads but OS threads are costly to spawn and schedule beyond a certain number. Virtual threads are simply cheaper threads as the scheduling is done in userspace and the JVM can in theory be smart about it.
Oh so jvm virtual threads will suspend execution and yield without cooperation, just like an OS scheduler? That's a big difference to "just a task scheduler" which just queues tasks and distributes N tasks on M threads.