new Thread(() -> work()).start() is the smallest unit of OS-level concurrency: the kernel creates a schedulable task that shares your address space but owns its own stack and register state. The whole act is one syscall plus a scheduler decision — here is what executes.
The runtime's pthread_create (glibc) prepares: it allocates the thread's stack — an 8 MB anonymous mmap by default (RSS grows on touch, as in malloc-trace's page fault) — and fills a pthread_attr with flags and stack pointer. It then calls clone with flags that say share the address space (CLONE_VM), share file descriptors, signal tables, and the PID namespace's thread group. All of this is user-space setup, ~microseconds.
clone is the syscall — the kernel's copy_process runs. Unlike fork, there is no address-space copy: the new task points at the parent's mm, and the kernel instead allocates a new task_struct (~several KB), a kernel stack, a thread_info, and a fresh set of register state. On x86-64 this is the clone syscall (or clone3 with extensible args), ~10–50 µs of kernel work — mostly slab allocations and scheduler bookkeeping, no I/O.
The new task is now a first-class scheduler entity: it has a state (NEW), a static priority, a struct sched_entity on the CFS run queue, and a thread group leader. Critically it does not start running yet. The kernel returns to the spawning thread, which continues executing immediately — start() returns before the child's first instruction has run. Spawn is asynchronous by construction.
The scheduler picks the task — either immediately, if a CPU has an idle run queue or an expired timeslice to preempt, or later, whenever the load balancer or the next schedule() call favors it. The child's first run begins at the task's entry point (the clone return path with a synthetic frame that calls the thread function — the kernel-set up stack already has the return address of start_thread).
The first run costs a context switch: the kernel saves the previous task's registers and stack pointer, restores the child's, and returns to user space in the child — with a different CR3-free address space (shared mm means no TLB flush, one of the big wins of threads over processes) and a cold cache. The child's first instructions are compulsory cache misses: its stack pages are cold, its code pages are likely shared with the parent, but its working set is empty. The switch itself is ~1–5 µs (pure register save/restore plus scheduling decision); the cold-cache penalty adds microseconds more.
perf stat -e task:task_newtask,context-switches,page-faults -a sleep 0.1
# or time 10,000 spawns in your language of choice — typically 40–80 µs eachWhat the machine actually does is a two-step handoff: the kernel manufactures a full-fledged task — stack, register state, scheduler entry — in about 50 microseconds, then decides when it runs. The spawn is cheap and asynchronous; the scheduling is the part you don't control, and the first run is the part you pay for in cache misses.