Major nitpick: also virtual threads are cooperatively scheduled on carrier threads (the underlying platform threads). You "cooperate" implicitly by calling APIs that block the thread. There is no preemption if you hog the carrier thread with, say, number crunching or array sorting.
Straight from JEP 444:
> The scheduler does not currently implement time sharing for virtual threads. Time sharing is the forceful preemption of a thread that has consumed an allotted quantity of CPU time. While time sharing can be effective at reducing the latency of some tasks when there are a relatively small number of platform threads and CPU utilization is at 100%, it is not clear that time sharing would be as effective with a million virtual threads.
Straight from JEP 444:
> The scheduler does not currently implement time sharing for virtual threads. Time sharing is the forceful preemption of a thread that has consumed an allotted quantity of CPU time. While time sharing can be effective at reducing the latency of some tasks when there are a relatively small number of platform threads and CPU utilization is at 100%, it is not clear that time sharing would be as effective with a million virtual threads.