In traditional operating systems like Windows, processes and threads are treated as entirely different concepts. A process is a heavy container, and threads are lightweight execution units that live inside that container.
The Linux kernel takes a brilliantly unified approach. In Linux, there is absolutely no technical difference between a process and a thread at the structural level.
Threads are just Processes
To the Linux scheduler, a thread is just another task_struct in the execution queue.
When a Java Android app creates a new Thread to perform a background download, the Android runtime executes the clone() system call. The kernel creates a new task_struct for the thread, assigning it a unique Thread ID (TID).
The only difference between this new thread and a completely separate process is memory sharing.
- When Zygote forks a new app, the kernel gives the new app a completely isolated virtual memory space.
- When an app creates a thread via
clone(), the kernel sets a flag telling the newtask_structto point to the exact same virtual memory space as the parent.
Because they share the same memory pointers (mm_struct), threads can easily read and write to the same variables, making them "lightweight" and incredibly fast to create.
Thread IDs vs Process IDs
Because threads and processes use the same task_struct backend, their IDs can be confusing for developers looking at kernel logs.
- TID (Thread ID): The unique kernel identifier for the specific execution thread.
- PID (Process ID): The identifier for the overall application.
- TGID (Thread Group ID): In a multi-threaded app, all threads share the same TGID, which is equal to the TID of the original "main" thread.
When you run ps in the ADB shell, the "PID" column is actually displaying the TGID. If you want to see every individual thread running inside an Android app, you must use the -T flag.
Inspecting Threads via ADB
Let's look at the threads inside the system_server (the core of the Android framework).
adb shell ps -T -p <SYSTEM_SERVER_PID>
Output:
USER PID TID CMD
system 1500 1500 system_server
system 1500 1505 HeapTaskDaemon
system 1500 1510 ReferenceQueueD
system 1500 1542 ActivityManager
system 1500 1545 PackageManager
system 1500 1580 binder:1500_1
Here, you can clearly see that system_server is just a collection of threads. Notice how the TID is unique for every thread, but they all share the same PID (TGID) of 1500.
Android Thread Pools
Android applications rely heavily on threads to keep the UI responsive. The Main Thread (UI Thread) handles drawing the screen at 60 FPS. If you run a heavy database query on the Main Thread, the app will freeze and trigger an ANR (Application Not Responding) crash.
To manage this, the Android framework heavily utilizes Thread Pools.
Instead of asking the kernel to create and destroy a new task_struct every time a short background task is needed (which involves expensive context switching), Android creates a pool of sleeping threads.
When a task arrives via an ExecutorService, it wakes up an existing thread, assigns the task, and puts it back to sleep when finished, dramatically reducing kernel overhead.
// Example of an Android Thread Pool in action
ExecutorService threadPool = Executors.newFixedThreadPool(4);
threadPool.execute(new Runnable() {
@Override
public void run() {
// This runs on a pre-warmed background thread
performHeavyDatabaseQuery();
}
});