Opening
You use threads every single day. Every Android app you have ever built runs on threads. Your main thread draws the UI. Background threads fetch data from the network. And if you are using coroutines, there are threads under the hood doing the actual work. But here is a question most Android developers cannot answer: what is a thread, really? Not the Kotlin abstraction. Not the Java Thread class. What is a thread at the operating system level? What does the kernel actually create when you spawn one? Today we are going to open up the black box and look at what threads truly are, how the OS manages them, and why understanding this changes the way you think about concurrency in your Android apps.
A Thread Is Not What You Think
Most developers think of a thread as something that "runs code." And that is sort of true, but it misses the point. A thread is not a runner. A thread is a bookkeeping structure. It is the operating system's way of tracking an independent path of execution. When Linux, which is the kernel under Android, creates a thread, it allocates three things. First, a stack, a dedicated region of memory where that thread's function calls, local variables, and return addresses live. Second, a set of saved register values, so the CPU knows where this thread left off. And third, a block of metadata called the thread control block, which stores the thread's state, its priority, its ID, and scheduling information. That is it. A thread is a stack, a set of registers, and some bookkeeping. It is not a magical parallel universe. It is a data structure that the kernel manages.
Process vs Thread
Now before we go deeper, let us clear up something that confuses a lot of people: the difference between a process and a thread. A process is a running instance of a program. When you launch an Android app, the system creates a process for it. That process gets its own virtual address space, its own file descriptors, its own copy of the heap. It is an isolated sandbox. A thread lives inside a process. Multiple threads within the same process share the same heap memory, the same file descriptors, the same code. But each thread gets its own stack and its own register state. This is the key insight: threads within a process share memory. That is what makes them useful, because they can communicate by reading and writing shared variables. And that is also what makes them dangerous, because when two threads access the same data without coordination, things break in subtle and terrifying ways. We will get to that in a later video. For now, just remember: process equals isolation, thread equals shared memory with independent execution.
The Life of a Thread in the Kernel
So what does the kernel actually do with all these threads? It maintains a data structure, basically a big list, of every thread in the system. Each thread is in one of several states at any given moment. It might be running, meaning it is actively executing on a CPU core right now. It might be ready, meaning it could run but is waiting for a core to become available. It might be blocked, meaning it is waiting for something, like disk I/O or a lock or a network response. Or it might be sleeping, meaning it asked to be paused for a specific amount of time. The kernel's scheduler is constantly looking at this list and deciding which thread gets to run on which core and for how long. On a typical Android phone, there might be hundreds of threads across all running apps, but only four or eight CPU cores. The scheduler's job is to give the illusion that everything is running simultaneously, even though at any given instant, only a handful of threads are truly executing.
Thread Scheduling
Let us talk about how the scheduler makes those decisions. Linux uses a preemptive scheduling model. That means the kernel does not wait politely for a thread to finish. It forcibly interrupts threads after a time slice, typically a few milliseconds, and gives the CPU to someone else. This is how your phone can run Spotify, a messenger app, and your own app seemingly at the same time, even though there are way more threads than cores. The scheduler uses priorities to decide who goes first. On Android, the main thread of the foreground app gets a higher priority than background threads. This is why your UI stays responsive even when a background download is happening. The system is biased toward giving CPU time to the thread that the user is directly interacting with. There are also nice values and cgroup controls that Android uses to further prioritize foreground work. The key takeaway is that you do not control when your thread runs. The kernel does. Your thread might be in the middle of a calculation and get interrupted at any point. This has profound implications for correctness, as we will see when we discuss race conditions.
The Android Main Thread
Now let us connect this to something you know intimately: the Android main thread. When your app process starts, the system creates a single thread of execution. This is the main thread, sometimes called the UI thread. It is just a regular Linux thread with a special setup. It runs a message loop, the Looper, which pulls messages and runnables from a MessageQueue and executes them one at a time. Every UI operation, every lifecycle callback like onCreate and onResume, every click handler, every RecyclerView bind, all of these run on the main thread. And here is the critical constraint: because the main thread processes one message at a time, if any single message takes too long, the whole UI freezes. That is what an ANR is. The system detects that the main thread has not processed a new message for about five seconds, and it shows the "Application Not Responding" dialog. So the main thread is not special hardware. It is a regular thread with a loop. But it is the single most important thread in your app, because it is the one the user sees and feels.
Why Threads Are Expensive
Now here is something that surprises a lot of developers: threads are expensive. Not in the "oh it costs a couple of microseconds" way. Genuinely expensive. Each thread on Android gets a stack that is typically around one megabyte. One megabyte. That means if you create one hundred threads, you have just consumed a hundred megabytes of memory just for their stacks, not counting any work they are doing. On a phone with two or three gigabytes of RAM, that is significant. On top of the memory cost, each thread adds overhead to the kernel's scheduler. The more threads exist, the more work the scheduler has to do to decide who runs next. And there is the cost of context switching, which we will cover in the next video, but the short version is that switching between threads is not free. It costs real CPU time and thrashes your processor's caches. This is why you should not just create threads willy-nilly. The old pattern of new Thread, passing a runnable, calling start, was fine for simple cases, but it does not scale. Thread pools exist because creating and destroying threads on demand is wasteful. And coroutines exist partly because they can achieve concurrency without needing one thread per concurrent task.
Creating Threads in Kotlin
Let us look at what creating a thread actually looks like in Kotlin and what happens underneath. The simplest way is to call the thread function from the Kotlin standard library. You write thread, open a block, and put your code inside. Under the hood, this creates a new Java Thread object, which calls pthread_create on Linux, which asks the kernel to allocate a new stack, create a new task struct, and add it to the scheduler's list. All of that happens before your first line of code inside the block even executes. You can also use Executors, which maintain a pool of pre-created threads. When you submit work to an executor, it assigns it to an existing thread rather than creating a new one. This is far more efficient for repeated work. And then there are coroutines, which sit on top of thread pools. When you launch a coroutine, it does not create a new thread. It schedules a chunk of work to run on an existing thread from a pool. The coroutine can suspend, releasing that thread back to the pool, and resume later on a possibly different thread. This is the fundamental innovation: decoupling the unit of work from the unit of execution.
Thread.sleep vs delay
Here is a practical example that illustrates why threads and coroutines are fundamentally different. Consider Thread.sleep. When you call Thread.sleep for one second, your thread is blocked. The kernel marks that thread as sleeping, and it sits there consuming its one megabyte stack, doing absolutely nothing, for a full second. No other work can happen on that thread until it wakes up. Now consider delay, the coroutine version. When you call delay for one second, the coroutine suspends. It saves its state and the thread it was running on is released back to the pool, free to execute other coroutines. When the second is up, the coroutine resumes, possibly on a different thread entirely. The result is the same from the developer's perspective: code pauses for one second. But the cost is vastly different. Thread.sleep holds a thread hostage. delay lets the thread go help someone else. If you have a thousand concurrent tasks each waiting for one second, Thread.sleep requires a thousand threads, consuming a gigabyte of stack memory. delay requires only a handful of threads from the pool, reusing them efficiently. This is not a small optimization. This is a fundamental difference in resource usage.
What Happens in Memory
Let me paint a picture of what this looks like in your device's memory. Imagine your app process. It has one large heap that all threads share, where your objects, your ViewModels, your lists, your bitmaps all live. Then, off to the side, there are separate stacks, one per thread. The main thread has its stack. If you have created three background threads, there are three more stacks. Each stack is a contiguous block of memory, growing downward as functions are called and shrinking as they return. The kernel keeps track of each stack's boundaries and will kill your process with a segfault if any thread tries to grow its stack past its limit. The register state for each thread is saved in kernel memory when the thread is not actively running. When the scheduler picks a thread to run, it loads that thread's saved registers into the CPU and points the stack pointer at that thread's stack. The CPU then resumes execution exactly where that thread left off. This loading and unloading of state is the context switch, and it is the topic of our next video.
Recap and What is Next
Let us pull it all together. A thread is not magic. It is a stack, a set of register values, and a kernel data structure. The operating system manages all threads, scheduling them onto CPU cores and switching between them rapidly to create the illusion of parallel execution. Processes provide isolation. Threads within a process share memory. The Android main thread is just a regular thread running a message loop, and blocking it causes ANRs. Threads are expensive, about one megabyte each, which is why thread pools and coroutines exist to amortize that cost. And Thread.sleep blocks a real thread, while coroutine delay simply suspends and gives the thread back. In the next video, we are going to zoom into that moment when the kernel switches from one thread to another. What actually happens during a context switch, how much it costs, and why it matters so much for the performance of your Android apps. See you there.