Go vs Java — How Concurrency Works Under the Hood
Same word, two machines
"Thread" means different things in the two runtimes, and that single fact explains most of the practical
differences. Java uses 1:1 threading: every java.lang.Thread is one
real OS thread the kernel schedules. Go uses M:N: the runtime multiplexes many
goroutines (G) onto a small set of OS threads (M) via logical processors (P) — the GMP
scheduler.
Why that changes everything
Java Thread | Go goroutine | |
|---|---|---|
| Mapping | 1:1 with an OS thread | M:N — many per OS thread |
| Initial stack | ~512KB–1MB (fixed) | ~2KB (grows/shrinks on demand) |
| Created by | the OS (syscall) | the Go runtime (cheap) |
| Context switch | kernel, ~1–10 µs | user space, ~tens of ns |
| Practical max | thousands | hundreds of thousands+ |
| Scheduler | OS, preemptive | Go runtime, cooperative + async-preemptive (1.14+) |
| Blocking syscall | ties up its OS thread | runtime parks the G, reuses the M for other Gs |
Because a goroutine is ~500× cheaper than a thread, Go's idiom is "just start a goroutine per task." Java's
expensive threads are why the ecosystem leans on bounded thread pools
(ExecutorService) — you reuse a few OS threads instead of spawning many.
The blocking-I/O difference (the big one)
Block a Java thread on a socket read and that OS thread is stuck doing nothing until data arrives — 10,000 blocked connections need ~10,000 threads. Go's runtime intercepts the blocking call: it hands the network wait to an internal netpoller and frees the OS thread to run other goroutines, so 10,000 connections can be 10,000 goroutines on a handful of threads. This is why Go feels effortless for high-concurrency network servers.
// Go: 100,000 goroutines — fine
for i := 0; i < 100_000; i++ { go handle(i) }
// Java (classic): 100,000 threads — OutOfMemoryError / thrashing
for (int i = 0; i < 100_000; i++) new Thread(() -> handle()).start(); // don't
Java is closing the gap: virtual threads (Project Loom)
JDK 21 added virtual threads — the JVM's own M:N green threads scheduled onto a small carrier
pool, with the runtime parking them on blocking I/O. It brings Go's "a thread per request" model to Java:
Thread.ofVirtual().start(task) or Executors.newVirtualThreadPerTaskExecutor(). The
synchronization tools (locks, atomics) are unchanged — only the scheduling model caught up.
So which, when?
- Massive concurrent I/O (many sockets/RPCs): Go's goroutines (or Java virtual threads) win on simplicity and footprint.
- CPU-bound parallelism: both cap at core count; the model matters less — pin work to cores.
- Shared mutable state with complex invariants: Java's mature locks/concurrent collections are battle-tested; Go nudges you to redesign around channel ownership instead.
Takeaways
- Java 1:1 (Thread = OS thread, heavy) vs Go M:N (goroutines multiplexed by the GMP scheduler, cheap).
- Cheap goroutines → "one per task"; expensive threads → thread pools.
- Go parks goroutines on blocking I/O and reuses the OS thread — the key to its network-server scalability.
- Virtual threads (JDK 21) give the JVM the same M:N model; the locking APIs don't change.
Re-authored for this guide; GMP-vs-1:1 scheduler diagram hand-authored as SVG. Follows the Go runtime scheduler design (Dmitry Vyukov), the Go memory model, and the JEP 444 (Virtual Threads) documentation. See also: Go Concurrency — Goroutines, Channels & CSP; Thread pools & Executors.
🤖 Don't fully get this? Learn it with Claude
Stuck on Go vs Java — How Concurrency Works Under the Hood? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Go vs Java — How Concurrency Works Under the Hood** (Concurrency) and want to truly understand it. Explain Go vs Java — How Concurrency Works Under the Hood from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Go vs Java — How Concurrency Works Under the Hood** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Go vs Java — How Concurrency Works Under the Hood** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Go vs Java — How Concurrency Works Under the Hood** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.