All chapters in this track
- 01Chapter 1: Concurrency vs. Parallelism, and Why It's Hard
- 02Chapter 2: Processes vs. Threads, Revisited for Concurrency
- 03Chapter 3: The Core Hazards: Race Conditions, Deadlock, Livelock, Starvation
- 04Chapter 4: Synchronization Primitives: Locks, Semaphores, Monitors, Condition Variables
- 05Chapter 5: Designing for Thread Safety: Immutability, Confinement, Thread-Safe Collections
- 06Chapter 6: Classic Patterns: Producer-Consumer, Reader-Writer, Thread Pools
- 07Chapter 7: Higher-Level Models: Async/Await, Event Loops, Coroutines, Actors, Channels
- 08Chapter 8: Memory Models and Visibility: Happens-Before, Volatile, Atomics
- 09Chapter 9: Lock-Free and Wait-Free Programming Basics
- 10Chapter 10: How Concurrency Differs Across Runtimes: JVM Threads, Python's GIL, Go's Goroutines, JavaScript's Event Loop
- 11Chapter 11: Debugging and Testing Concurrent Code
- 12Chapter 12: Classic Interview Problem: Producer-Consumer
- 13Chapter 13: Classic Interview Problem: Dining Philosophers
- 14Chapter 14: Classic Interview Problem: Readers-Writers
- 15Chapter 15: Concurrency in System Design and Production Debugging
Chapter 10 of 15
Chapter 10: How Concurrency Differs Across Runtimes: JVM Threads, Python's GIL, Go's Goroutines, JavaScript's Event Loop
Why this matters
Every concept in this book so far is conceptually universal, but its practical consequences differ dramatically depending on which runtime you're actually working in — and nowhere is this more consequential, or more commonly misunderstood, than Python's Global Interpreter Lock. This chapter explains the GIL precisely, then surveys three other major runtimes' fundamentally different approaches, so you can reason correctly no matter which one you're working in.
Python's GIL, explained precisely
[SENIOR INSIGHT] CPython (the standard, most widely-used Python implementation) has a Global Interpreter Lock (GIL) — a single, process-wide lock that only one thread can hold at a time, and a thread must hold the GIL to execute any Python bytecode at all. This means true parallel execution of Python code across multiple threads, in CPython, is not possible — no matter how many threads you create, only one Python bytecode instruction executes at any literal instant.
import threading
import time
def cpu_bound_work():
total = 0
for i in range(50_000_000):
total += i
return total
# Single-threaded:
start = time.time()
cpu_bound_work()
print(f"Single-threaded: {time.time() - start:.2f}s")
# "Parallel" with 2 threads -- but NOT actually faster, because of the GIL:
start = time.time()
t1 = threading.Thread(target=cpu_bound_work)
t2 = threading.Thread(target=cpu_bound_work)
t1.start(); t2.start()
t1.join(); t2.join()
print(f"Two threads: {time.time() - start:.2f}s")
# The two-thread version does roughly the SAME total wall-clock time as running
# the work twice sequentially would -- NOT half, because the GIL serializes
# actual bytecode execution regardless of thread count.
Why the GIL exists: it dramatically simplifies CPython's internal memory management (reference counting for garbage collection, specifically, becomes trivially thread-safe if only one thread ever touches Python objects at a time) — a genuine engineering trade-off made early in CPython's history, not an oversight.
What the GIL does NOT prevent: I/O-bound concurrency — a thread waiting on network or disk I/O releases the GIL during that wait (since it's not executing Python bytecode during a blocking syscall), letting other threads run — which is exactly why threading remains genuinely useful in Python for I/O-bound work (the exact case Chapter 6's ThreadPoolExecutor example used) even though it provides zero benefit for CPU-bound work.
CPU-bound work + threading: NO speedup (GIL serializes bytecode execution)
I/O-bound work + threading: REAL speedup (GIL released during I/O waits)
CPU-bound work + multiprocessing: REAL speedup (separate processes, separate GILs)
[IN PRACTICE] This single fact — the GIL applies to bytecode execution, not to waiting — is the most commonly misunderstood aspect of Python concurrency, and getting it right explains exactly when threading helps (I/O-bound) versus when multiprocessing (Chapter 2) is required instead (CPU-bound).
The JVM: true thread-level parallelism, native memory model
[BEGINNER] The Java Virtual Machine has no GIL-equivalent — Java threads can execute genuinely in parallel across multiple CPU cores, which is exactly why Java's synchronization primitives (synchronized, java.util.concurrent's locks, atomics) and its formally-specified memory model (Chapter 8's happens-before relationships are drawn directly from the Java Memory Model specification) matter so directly and immediately — every hazard in Chapter 3, and every visibility concern in Chapter 8, is a first-order, constant concern in Java in a way it simply isn't in single-GIL CPython.
Go's goroutines and channels
[BEGINNER] Go takes a different approach: goroutines are extremely lightweight, runtime-managed logical threads (far cheaper to create than OS threads — a program can run hundreds of thousands of them), multiplexed onto a smaller number of actual OS threads by Go's own runtime scheduler, and Go's standard idiom strongly favors channels (Chapter 7's CSP model) over shared-memory locking for communication between goroutines — Go's own documentation states this philosophy explicitly: "don't communicate by sharing memory; share memory by communicating." Go achieves genuine parallelism (goroutines can run on multiple actual cores simultaneously, no GIL-equivalent restriction), while encouraging a programming style that sidesteps many of Chapter 3's hazards by design, through the channel-based architectural discipline from Chapter 7.
JavaScript's single-threaded event loop
[BEGINNER] JavaScript (in browsers and in Node.js) runs application code on a single thread, with an event loop conceptually similar to Python's asyncio (Chapter 7) — I/O operations are non-blocking, and callbacks/promises resume execution at specific points, never preemptively. This means JavaScript code between any two suspension points (an await, the end of a synchronous callback) never races with other JavaScript code, since there is fundamentally only one thread ever executing JavaScript at all — a genuinely different, and in some ways simpler, foundational guarantee than any of the other three runtimes discussed in this chapter. (Modern JavaScript does support genuine multi-threading via Web Workers/worker threads, but each worker has its own fully isolated memory — communication happens via explicit message passing, directly the actor-model-like discipline from Chapter 7, not shared memory.)
A comparison table
| Runtime | True parallel execution of its own code? | Primary concurrency hazard exposure |
|---|---|---|
| CPython | No, for CPU-bound work (GIL); yes, for I/O-bound waiting | Chapter 3's hazards apply fully to I/O-bound multi-threaded code; CPU-bound work needs multiprocessing instead |
| JVM (Java) | Yes, fully | Chapter 3's hazards and Chapter 8's visibility concerns are constant, first-order concerns |
| Go | Yes, fully (via goroutines on OS threads) | Mitigated by strong channel/CSP idioms (Chapter 7), though shared-memory hazards remain possible if channels are bypassed |
| JavaScript (main thread) | No — single-threaded by design | No same-thread races possible; Web Workers reintroduce true parallelism via isolated, message-passing units |
Common mistakes
- Assuming Python's
threadingmodule behaves like Java's threads, expecting a CPU-bound speedup that the GIL specifically prevents — this is the single most common cross-language concurrency misconception. - Assuming the GIL means Python threads are useless for concurrency generally — it remains genuinely valuable for I/O-bound work specifically, since the GIL is released during I/O waits.
- Writing Go code with heavy shared-memory locking out of habit (from a background in Java or C++) instead of leaning into Go's idiomatic channel-based communication style, missing much of the language's designed-in safety benefit.
- Not realizing JavaScript's Web Workers use isolated memory with message passing, and expecting shared-memory-style direct access between a worker and the main thread the way threads within one process work in other languages.
Discussion / interview questions
-
"What is Python's GIL, and why doesn't multi-threading speed up CPU-bound Python code?" Strong answer: The GIL is a single, process-wide lock that only one thread can hold at a time, required to execute any Python bytecode — this serializes bytecode execution regardless of thread count, so CPU-bound work sees no speedup from more threads;
multiprocessing(separate processes, separate GILs) is needed for genuine CPU-bound parallelism instead. -
"Why does Python's
threadingmodule still provide real value for I/O-bound work, despite the GIL?" Strong answer: A thread releases the GIL while blocked on I/O (since it's not executing Python bytecode during a blocking syscall), allowing other threads to run during that wait — this is exactly the concurrency (not parallelism, per Chapter 1) benefit that helps I/O-bound workloads. -
"How does Go's approach to concurrency differ philosophically from Java's?" Strong answer: Go strongly favors channel-based communication between lightweight, runtime-managed goroutines ("share memory by communicating") over Java's more traditional shared-memory-plus-locks model, while both achieve genuine parallel execution across cores — Go's idiomatic style sidesteps many classic hazards by architectural discipline rather than eliminating the underlying possibility of them entirely.
-
"Why can't two pieces of JavaScript code running on the main thread ever have a classic race condition with each other?" Strong answer: JavaScript's main thread executes only one piece of code at a time, with switching happening only at explicit suspension points (via the event loop) rather than preemptively — there's fundamentally only one thread, so there's no possibility of two pieces of main-thread code executing simultaneously and racing over shared state, though logical races across await/callback boundaries (Chapter 7's async warning) are still possible.
Chapter summary / cheat sheet
- CPython's GIL: serializes bytecode execution — no CPU-bound speedup from threading, but real I/O-bound concurrency benefit since the GIL releases during I/O waits;
multiprocessingis needed for genuine CPU-bound parallelism. - JVM (Java): true thread-level parallelism, no GIL-equivalent — Chapter 3's hazards and Chapter 8's visibility concerns are constant, first-order concerns requiring explicit synchronization.
- Go: lightweight goroutines with true parallelism, strongly idiomatic channel-based ("share memory by communicating") communication style.
- JavaScript: single-threaded main-thread execution with an event loop (no same-thread races possible); Web Workers add true parallelism via fully isolated memory and message passing.
- The practical lesson: know your specific runtime's actual concurrency model before assuming a technique or assumption from one language transfers directly to another — the GIL misconception is the single most common cross-language error in this space.