Thread in mobile development — what it is, types and thread management

Author: IT Sectr Published: 2026-03-16 Reading time: 11 min

Thread is the basic unit of CPU time, having its own stack and executing independently of other threads. In mobile development, threads are used for parallel task execution to keep the interface responsive during long-running operations. Android supports java.lang.Thread, Executors and Kotlin Coroutines, iOS — Thread (Objective-C), GCD and OperationQueue. According to Android Thread Documentation, creating a native thread requires allocating ~1 MB for the stack by the operating system.

Key Takeaways

  • Thread — the minimum CPU scheduling unit: each thread is independent and has its own stack
  • Creating a thread requires ~1 MB stack in Android and 512 KB in iOS, so pools are more efficient than direct creation
  • Android: Thread, Executors, HandlerThread, Coroutines — four levels of thread abstraction
  • iOS: Thread (low-level), GCD (DispatchQueue), OperationQueue (high-level)
  • Thread safety — shared access to mutable data requires synchronization: locks, atomic, serial queues

What is Thread

Thread (execution thread) is an independent sequence of instructions that the operating system can schedule on a CPU core. Each process (application) contains at least one thread — the Main Thread. Additional threads are created for parallel task execution. Each thread has its own program stack (with local variables), program counter (PC) and registers. The heap memory is shared among all threads of the process.

In mobile operating systems, threads are scheduled using preemptive multitasking: the OS can interrupt a thread’s execution at any moment and hand control to another (context switch). Context switching is an expensive operation (1–10 microseconds) as it requires saving/restoring CPU registers, updating the TLB and flushing caches. This is why an excessive number of threads (hundreds or thousands) degrades performance — the OS spends more time switching than executing.

Thread vs Process — different concepts. A process is an instance of an application with allocated virtual memory. A thread within a process shares this memory with other threads. In Android, each application component (Activity, Service, BroadcastReceiver) works in one process but can execute on different threads. An iOS application is also a single process capable of creating additional threads through GCD or Thread.

Thread Lifecycle: States and Transitions

Each thread in Java/Kotlin (Android) and NSThread (iOS) goes through five states: New (created), Runnable (ready to execute), Running (executing on CPU), Blocked/Waiting (waiting for a resource or notification), Terminated (finished). Transitions between states are managed by the OS scheduler and synchronization primitives. The developer can influence thread priority (Thread.setPriority()) and its state (sleep, join, interrupt).

In Android, a thread enters the Blocked state when trying to acquire a busy monitor (synchronized), calling Object.wait() or Thread.sleep(). In iOS — when calling NSCondition.wait(), pthread_cond_wait() or dispatch_semaphore_wait(). In the Blocked state, the thread does not consume CPU but still occupies memory (stack). A thread can be interrupted from another thread, receiving InterruptedException (Java) or checking isCancelled (Kotlin Coroutines).

StateDescriptionTransition Method
NewThread created but not startedThread() constructor
RunnableThread ready to execute, waiting for CPUthread.start()
RunningThread executing on CPU coreOS Scheduler
Blocked/WaitingThread waiting for a resource, monitor or notificationsynchronized, wait(), sleep()
TerminatedThread completed run() or was interruptedrun() completed, interrupt()

Context Switch and Its Cost

Context switch is an operation where the OS saves the current thread’s state (registers, PC, TLB) and loads the saved state of another. In mobile systems (Linux + ART, XNU for iOS) a context switch takes 1–10 microseconds. If a thread executes a task in 100 microseconds and a context switch takes 5, then 5% of time is wasted. To minimize context switching, iOS uses GCD with work stealing, Android uses pools with fixedThreadCount.

Thread in Android: From Thread to Coroutines

Android has evolved from low-level java.lang.Thread to modern coroutines. Each abstraction level provides more capabilities with lower overhead. Thread is the base class, but its direct creation is not recommended: a new thread is not managed by a pool, it’s hard to monitor and cancel. AsyncTask (deprecated since API 30) was a step forward, but suffered from memory leaks and inconvenient configuration handling.

HandlerThread is a special Thread subclass with Looper that can process a message queue. It is used for sequential execution of tasks on a background thread, for example, writing data to Room or files. HandlerThread is created by calling start(), after which messages and Runnable can be sent via Handler(handlerThread.looper). Calling handlerThread.quit() stops the Looper and terminates the thread.

kotlin
// Android: Thread, HandlerThread and Executors
import android.os.Handler
import android.os.HandlerThread
import java.util.concurrent.Executors

class ThreadExample {

    // 1. Direct Thread creation (not recommended)
    fun directThread() {
        val thread = Thread(Runnable {
            Thread.sleep(1000)
            print("Direct thread executed")
        })
        thread.start()
    }

    // 2. HandlerThread for sequential background tasks
    fun handlerThreadExample() {
        val handlerThread = HandlerThread("BackgroundQueue")
        handlerThread.start()

        val handler = Handler(handlerThread.looper)
        handler.post {
            // Sequential execution on a background thread
            Thread.sleep(500)
            print("HandlerThread: task completed")
        }

        // Stopping the thread (executed when tasks are finished)
        handlerThread.quitSafely()
    }

    // 3. Executors — thread pool
    fun executorExample() {
        val executor = Executors.newFixedThreadPool(4)
        for (i in 1..10) {
            executor.execute {
                print("Task $i on thread ${Thread.currentThread().getName()}")
            }
        }
        executor.shutdown()
    }

    // 4. Kotlin Coroutines — modern standard
    suspend fun coroutineExample() = kotlinx.coroutines.withContext(
        kotlinx.coroutines.Dispatchers.Default
    ) {
        print("Coroutine on thread: ${Thread.currentThread().getName()}")
    }
}

The ThreadExample demonstrates all four levels of thread abstraction in Android. Direct Thread creation is the lowest-level and most inefficient approach. HandlerThread is useful for sequential background tasks. Executors.newFixedThreadPool(4) creates a pool of 4 threads for parallel execution of up to 10 tasks. Kotlin Coroutines with Dispatchers.Default is a modern, efficient and safe approach.

HandlerThread: Sequential Background Tasks

HandlerThread is a specialized Thread subclass with a built-in Looper and message queue. It is created by calling start(), after which Runnable and messages can be sent via Handler(handlerThread.looper). HandlerThread executes tasks strictly sequentially — the next task does not start until the previous one completes. This is convenient for writing data to Room or files, where operation order is critical. Calling quitSafely() stops the Looper after the current task completes.

Thread in iOS: Thread, GCD and OperationQueue

iOS also provides three levels of thread management. Thread (Thread in Swift, NSThread in Objective-C) is a low-level API that directly creates a native thread. GCD (Grand Central Dispatch) through DispatchQueue is the primary tool for iOS developers, automatically managing a thread pool. OperationQueue is a high-level abstraction over GCD with support for dependencies, priorities and cancellation.

Direct use of Thread in modern iOS development is extremely rare — GCD provides all the necessary capabilities with automatic memory and thread management. Thread is used only for specific cases: setting thread-local storage (threadDictionary), creating a RunLoop for a background thread, or integrating with C libraries expecting pthread_t.

swift
import Foundation

class ThreadManager {

    // 1. Thread (low-level)
    func createThread() {
        let thread = Thread {
            // Code executing on a new thread
            print("Current thread: \(Thread.current)")
        }
        thread.name = "com.app.worker"
        thread.qualityOfService = .utility
        thread.start()
    }

    // 2. GCD — DispatchQueue
    func gcdExample() {
        // Concurrent queue
        let queue = DispatchQueue(label: "com.app.concurrent",
                                 qos: .utility,
                                 attributes: .concurrent)

        queue.async {
            print("GCD async task")
        }

        // Barrier for write synchronization
        queue.async(flags: .barrier) {
            // Exclusive access during write
            print("Barrier write: exclusive access")
        }
    }

    // 3. OperationQueue with dependencies
    func operationQueueExample() {
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 2
        queue.qualityOfService = .background

        let download = BlockOperation {
            print("Downloading...")
        }
        let process = BlockOperation {
            print("Processing...")
        }
        let save = BlockOperation {
            print("Saving...")
        }

        // Dependencies: download -> process -> save
        process.addDependency(download)
        save.addDependency(process)

        queue.addOperations([download, process, save], waitUntilFinished: false)
    }
}

// Thread-safe collection via GCD barrier
class ThreadSafeArray<T> {
    private var array: [T] = []
    private let queue = DispatchQueue(label: "com.app.concurrent",
                                       attributes: .concurrent)

    var count: Int {
        return queue.sync { array.count } // concurrent read
    }

    func append(_ element: T) {
        queue.async(flags: .barrier) { // exclusive write
            self.array.append(element)
        }
    }
}

The ThreadSafeArray class demonstrates the Concurrent Read / Exclusive Write pattern using GCD barrier. Reading via queue.sync{} executes in parallel from multiple threads. Writing via queue.async(flags: .barrier) blocks all other operations (both reads and writes) until the write completes. This is more efficient than synchronized blocks as it does not block readers when there is no write.

iOS Thread vs GCD: When to Use Thread Directly

Direct use of Thread in iOS is justified in three cases: for thread-local storage (Thread.current.threadDictionary) — storing data bound to a thread; for creating a special RunLoop on a background thread with performSelector:onThread:; for integration with C/C++ libraries that expect pthread_t. In all other cases, GCD via DispatchQueue is preferable — it automatically manages the thread pool and power consumption.

Thread Synchronization: Locks, Atomic, Serial Queues

Race condition occurs when two or more threads simultaneously access shared data and at least one thread is writing. The result depends on execution timing and is unpredictable. Synchronization primitives are used to prevent race conditions. In mobile development, available primitives include locks (synchronized, NSLock), atomic operations (AtomicInteger, iOS atomic properties) and queues (serial queue).

Primitive selection depends on the scenario. For simple counters and flags, atomic operations are sufficient (AtomicInteger, atomic property). For critical sections with multiple operations — locks (synchronized, NSLock). For complex data structures — serial DispatchQueue or GCD barrier. Locks are easier to understand but are prone to deadlocks and livelocks. Queues are more complex but safer.

kotlin
// Synchronization in Android/Kotlin
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

class Counter {

    // 1. AtomicInteger — for simple counters
    private val atomicCount = AtomicInteger(0)
    fun incrementAtomic() = atomicCount.incrementAndGet()

    // 2. synchronized — for critical sections
    @Synchronized
    fun synchronizedOperation() {
        // Only one thread at a time
        doWork()
    }

    // 3. Coroutine Mutex — suspend-safe
    private val mutex = Mutex()
    suspend fun mutexOperation() {
        mutex.withLock {
            // protected code — thread-safe
            doWork()
        }
    }

    private fun doWork() { /* critical section */ }
}

// Deadlock example: A locks B, B locks A
class DeadlockExample {
    private val lockA = Any()
    private val lockB = Any()

    fun methodA() = synchronized(lockA) {
        Thread.sleep(100)
        synchronized(lockB) { print("OK") }
    }

    fun methodB() = synchronized(lockB) {
        Thread.sleep(100)
        synchronized(lockA) { print("OK") }
    }
}

Counter demonstrates three synchronization approaches. AtomicInteger.incrementAndGet() — atomic operation without locks (CAS). @Synchronized — Java’s built-in monitor, locks the entire object. Mutex.withLock — coroutine mutex, suspends the coroutine instead of blocking the thread (more efficient). DeadlockExample shows a classic deadlock: two threads acquire locks in different order.

Thread Pools: Why Executors are Better than Thread

Thread Pool is a set of pre-created threads that are reused for task execution. Instead of creating a new thread for each task (expensive), the pool takes a free thread from the pool. If no threads are free, the task is queued. The pool automatically manages its size: new threads are created under peak loads, idle threads are terminated. This reduces thread creation overhead by dozens of times.

In Android, Executors.newFixedThreadPool(4) creates a pool of 4 threads. If 10 tasks arrive simultaneously, 4 start executing immediately, 6 wait in the queue. Executors.newCachedThreadPool() creates threads as needed (no limit) and terminates idle ones after 60 seconds. For iOS, GCD automatically provides global queue pools whose size corresponds to the number of CPU cores and current load.

In Kotlin Coroutines, thread pools are hidden inside dispatchers. Dispatchers.Default uses a pool size = number of CPU cores (minimum 2). Dispatchers.IO — 64 threads (sufficient for hundreds of IO-bound tasks since most will be waiting for I/O, not occupying CPU). Each dispatcher automatically scales the pool under load, saving battery power when idle.

Frequently Asked Questions

What is a Thread in mobile development?

Thread is the basic unit of code execution in an application. Each process can have multiple threads sharing memory but with their own stack. In mobile development, threads are used for parallel task execution without blocking the UI. Android uses Thread, Executors, HandlerThread and Coroutines. iOS uses Thread, GCD (DispatchQueue) and OperationQueue.

Why is it not recommended to create Thread directly?

Creating a Thread requires allocating ~1 MB stack in Android and ~512 KB in iOS — this is an expensive operation. For 1000 tasks, directly creating 1000 threads would require ~1 GB just for stacks plus overhead for context switching. Instead of Thread, use pools (Executors, GCD) or coroutines — they reuse threads, reducing overhead by dozens of times.

What is a race condition and how to avoid it?

Race condition is unpredictable behavior when multiple threads simultaneously access shared data with writes. It can be avoided in three ways: use atomic types (AtomicInteger), locks (synchronized, NSLock), or serialize access through a queue (DispatchQueue serial, Kotlin Actor). Best practice is to minimize shared mutable state and use immutability.

How is Thread different from coroutine?

Thread is a native system object occupying ~1 MB of stack and bound to the OS kernel. Coroutine is a lightweight Kotlin execution unit that is not tied to a specific thread and can suspend without blocking. One thread can execute thousands of coroutines. Coroutines are more memory efficient and allow writing asynchronous code without callbacks.

How to catch a deadlock in a mobile application?

Deadlock manifests as a complete application freeze without ANR. In Android, use Thread.getAllStackTraces() to dump all thread stacks — two threads will be waiting for each other’s locks. In iOS — Thread.callStackSymbols. Tools: Android Studio Profiler (Threads tab), Instruments (iOS, Thread State View). Prevention: acquire locks in a fixed order, use tryLock with timeout.

Summary

  • Thread — the minimum CPU unit: independent execution with its own stack, shared heap memory
  • Five states of a thread: New, Runnable, Running, Blocked/Waiting, Terminated
  • Android evolved from Thread → AsyncTask → Executors → HandlerThread → Coroutines
  • iOS provides Thread, GCD (DispatchQueue) and OperationQueue — from low to high level
  • Race condition is solved by locks (synchronized, NSLock), atomic types and serial queues
  • Deadlock occurs from cross-lock acquisition — prevented by fixed ordering
  • Thread Pool is more efficient than creating new Threads: reuses threads, reduces context switch overhead

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also