Interview Prep Zoneby CuriouserLabs

Question 4 of 5 · staff level

User-level vs kernel-level threads and the 1:1 / N:M models — why did green threads come back?

🎤 Say this first

Kernel (1:1) threads: every app thread is a kernel task — true parallelism, standard tooling, but creation/switch costs and ~MB stacks cap you at thousands. User-level (N:M) threads: a runtime multiplexes many light threads over few kernel threads — millions possible, nanosecond switches, but the kernel can't see them, so one blocking syscall used to stall a whole carrier. Green threads died in the 90s because runtimes couldn't solve blocking; they returned (Go goroutines, Java virtual threads) once runtimes owned the entire I/O stack and could unmount a blocked thread instead of blocking the carrier.

The full picture

  • Why 1:1 won for 20 years: simplicity and honesty — the kernel schedules everything, blocking just works, debuggers/profilers see reality. Java dropped its original green threads for exactly this.
  • What changed: Go and the JDK rewrote their I/O layers so 'blocking' calls park the user-level thread and release the carrier (Java: unmount + continuation to heap; JEP 444). The N:M scheduler lives in the runtime (ForkJoin carriers ≈ cores).
  • Remaining leaks in the abstraction: syscalls the runtime can't intercept (some file ops, JNI/FFI) still pin/consume a carrier — Go spawns extra Ms, Java compensates the FJ pool; and CPU-bound user threads don't preempt as fairly as the kernel would.
  • Interview framing: thread-per-request coding model at event-loop scalability — the runtime moved the async complexity below the programming model instead of into your code (contrast callbacks/reactive).

🔄 Likely follow-up questions

  • What is a continuation and how does unmounting actually work?
  • How do goroutines differ from Java virtual threads in preemption?
  • Why do profilers historically struggle with N:M threading?