Starvation in Mobile Applications — What It Is, Causes, and How to Prevent Thread Starvation

Author: IT Sectr Published: 2026-03-18 Reading time: 10 min

Starvation (thread starvation) is a situation in which a thread cannot access a resource required to continue its work, even though it is ready to execute. According to Baeldung (Java Thread Starvation, 2024), starvation occurs due to unfair scheduling, where low-priority threads are constantly postponed in favor of higher-priority ones. Unlike Deadlock, Starvation does not block the thread — it remains in the RUNNABLE state but never gets CPU time.

Key Takeaways

  • Starvation — a situation where a thread cannot access a resource despite being ready to execute
  • Unlike Deadlock, a starved thread remains in the RUNNABLE state — it is not blocked, but makes no progress
  • Unfair scheduling (e.g., synchronization via synchronized) — the main cause of Starvation on the JVM
  • Fair Lock (ReentrantLock(true)) guarantees fair FIFO order of lock acquisition
  • Thread Priority in mobile development is recommended not to be changed — Android Runtime manages priorities itself

What Is Starvation?

Starvation (thread starvation) is a multithreading problem where a thread cannot access a resource required to complete its task, even though the resource is not permanently locked by another thread. The thread is in the RUNNABLE state, but the scheduler or synchronization mechanism systematically postpones its execution in favor of other threads.

In mobile development, Starvation manifests as uneven task execution: some operations execute instantly, while others suffer catastrophic delays. For example, a background data synchronization thread may never gain access to the database if the UI thread and animation handlers constantly preempt it. According to the Android Developer Blog (Performance Matters, 2023), about 12% of missed frames (jank) on Android are caused by Starvation of background tasks on which rendering depends.

The key difference between Starvation and Deadlock is reversibility. If system load decreases or priorities are redistributed, the starving thread may acquire the resource and complete its work. However, under sustained high load, Starvation can last indefinitely, creating the impression of a frozen application.

Causes of Thread Starvation

Non-Fair Locks

synchronized in Java and Kotlin is a classic example of an unfair mechanism. Under high contention, the JVM may continuously grant the lock to the same active threads, while other threads constantly lose the race. This is not a JVM bug but a design trade-off: unfair locks provide higher throughput at the expense of access fairness. For mobile applications with 4–8 threads, this problem is especially relevant.

Improper Use of Priorities

Setting different thread priorities can lead to Starvation of low-priority threads. In Android Runtime, the Linux CFS (Completely Fair Scheduler) distributes CPU time proportionally to priorities, and if high-priority threads are constantly active, low-priority threads may never get CPU time. Google strongly discourages changing thread priorities in Android — the system manages them itself.

Long Critical Sections

If a thread holds a lock for too long (performing heavy computations, network requests, or file operations inside a synchronized block), other threads waiting for that lock starve. This is especially dangerous in Android, where long operations on the UI thread cause ANR, and moving them to background threads without optimizing critical sections merely transfers the Starvation problem to worker threads.

Starvation Code Example in Kotlin

Consider an example where one thread acquires a lock too frequently due to unfair scheduling. Starvation is demonstrated through an infinite loop of a high-priority thread that prevents a low-priority thread from accessing a shared resource.

kotlin
class SharedResource {
    private val lock = Any()

    fun criticalSection(id: String) {
        synchronized(lock) {
            println("$id got access")
            Thread.sleep(10)  // simulating work
        }
    }
}

fun main() {
    val resource = SharedResource()

    // High-priority thread — constantly active
    val highPriority = Thread {
        while (true) {
            resource.criticalSection("High")
        }
    }

    // Low-priority thread — may never get access
    val lowPriority = Thread {
        while (true) {
            resource.criticalSection("Low")
        }
    }

    highPriority.start()
    lowPriority.start()
    // "Low" may never print a message — Starvation!
}

In this example, the highPriority thread constantly acquires the lock and releases it for only 10 ms. Due to the unfair nature of synchronized, the JVM scheduler will most likely grant the lock again to the same thread that just released it — the low-priority thread starves. The solution is to use ReentrantLock(true) with the fair flag, which guarantees FIFO waiting order.

The corrected version with a fair lock ensures equitable distribution of resource access.

kotlin
class FairSharedResource {
    private val fairLock = ReentrantLock(true)  // fair = true

    fun criticalSection(id: String) {
        fairLock.lock()
        try {
            println("$id got access (fair)")
            Thread.sleep(10)
        } finally {
            fairLock.unlock()
        }
    }
}

Starvation vs Deadlock vs Livelock

Three classic multithreading problems — Starvation, Deadlock, and Livelock — are often grouped together, but their mechanisms and solutions differ. Starvation — the thread is ready but cannot acquire the resource. Deadlock — threads are blocked by cyclic waiting. Livelock — threads are active but make no progress.

ParameterStarvationDeadlockLivelock
Thread StateRUNNABLEBLOCKEDRUNNABLE
ProgressNoneNoneNone (though active)
CPU UsageLowMinimalHigh (up to 100%)
CauseUnfair schedulingCyclic waitingSame response to conflict
Main FixFair Lock, shorten critical sectionsLock hierarchyRetry limit, exponential backoff

Starvation is considered less critical than Deadlock because it is not fatal — under reduced load, the starving thread will eventually execute. However, in real-world Android usage, where memory and CPU are limited, Starvation can last for minutes, creating an unacceptable UX.

How to Detect Starvation

Thread Dump taken repeatedly at short intervals — the basic method for detecting starvation. If a thread is consistently in the RUNNABLE state but its call stack does not change across multiple dumps — this is a classic sign of Starvation. In Android Studio, use Android Profiler with thread state recording over time.

Automated detection is possible through execution time monitoring of tasks. If a task with a predictable execution time (e.g., 50 ms) takes 5 seconds or more — there is a high probability of Starvation. In mobile applications, Firebase Performance Monitoring allows you to set up custom traces for critical sections and receive notifications when thresholds are exceeded.

To diagnose Starvation caused by synchronized blocks, use Java Flight Recorder (JFR) (available on Android via the OpenJDK API) or Async Profiler. These tools show which monitors have the highest wait times and which threads compete for each monitor. JFR data integrates with IntelliJ IDEA Ultimate through its built-in profiler.

Methods to Prevent Thread Starvation

Fair Lock (ReentrantLock with true flag)

ReentrantLock(true) guarantees that threads acquire the lock in FIFO order. Unlike synchronized, a fair lock does not allow a thread that just released the lock to immediately reacquire it. This completely eliminates Starvation, although it reduces overall throughput by 10–20% due to the overhead of maintaining the queue.

Lock-Free Atomic Structures

Lock-free data structures (ConcurrentHashMap, AtomicReference, LongAdder) eliminate Starvation by definition, as they have no locks that can be held by one thread. All operations use CPU CAS instructions that guarantee at least one thread makes progress in a finite number of steps. For mobile development, prefer ConcurrentLinkedQueue for task queues.

Short Critical Sections

Minimizing lock hold time is a universal way to reduce the risk of Starvation. Move heavy operations (network, disk I/O, complex computations) outside synchronized blocks. Use ReadWriteLock for scenarios where readers should not starve due to infrequent writers. The Kotlin Coroutines library provides Mutex with a suspending mechanism that does not block an OS thread.

Condition Variables and Signals

Condition.await() and signal() should be used with caution: a thread waiting on a Condition wakes up together with other threads (spurious wakeup), and all compete for the lock. If one thread immediately returns to waiting after await while others manage to acquire the lock, the starving thread may wake up and go back to sleep indefinitely. Always check the condition in a while loop rather than an if statement to guarantee re-checking.

Frequently Asked Questions

What is the difference between Starvation and Priority Inversion?

Priority Inversion is a situation where a low-priority thread holds a lock needed by a high-priority thread. As a result, the high-priority thread waits for the low-priority one — priorities are inverted. Starvation is a broader problem: a thread cannot acquire a resource regardless of priority, due to unfair scheduling or long critical sections.

Can Starvation occur in a single-threaded application?

No, Starvation is a multithreading problem. Single-threaded code has no resource contention or thread scheduling. However, Starvation can occur in asynchronous single-threaded code (e.g., JavaScript event loop) if one microtask indefinitely postpones the execution of others via setTimeout with zero delay.

How is the Java Memory Model related to Starvation?

JMM (Java Memory Model) defines rules for visibility of changes between threads but does not guarantee fair scheduling. synchronized, in accordance with JMM, ensures sequential consistency — basic correctness — but does not prevent Starvation. Fairness requires additional mechanisms not specified in the JMM.

What is Starvation in the Android UI thread?

The UI thread (Main Thread) cannot starve in the classical sense because it has the highest priority. However, Starvation occurs when the UI thread waits for a result from a starved background thread. A typical scenario: an AsyncTask or coroutine loads data but cannot access the database due to contention with other threads, and the UI freezes while waiting.

How to prevent Starvation in Kotlin Coroutines?

In coroutines, to prevent Starvation, use limitedParallelism on Dispatchers.IO to avoid thread exhaustion. For synchronization, use Mutex from kotlinx.coroutines.sync — it suspends the coroutine rather than blocking the thread, reducing the risk of starvation. Avoid runBlocking in coroutines, as it can capture a pool thread and cause Starvation of other coroutines.

Summary

  • Starvation — a situation where a thread is ready to execute but cannot acquire a resource due to unfair scheduling
  • Unlike Deadlock, a starved thread remains in the RUNNABLE state and may execute when load decreases
  • Unfair locks (synchronized) and improper use of priorities are the main causes of Starvation
  • Fair Lock (ReentrantLock with true flag) guarantees FIFO access order and completely eliminates starvation
  • Lock-free structures (ConcurrentHashMap, AtomicReference) eliminate Starvation at the architecture level
  • Thread Dump with repeated captures and Java Flight Recorder are effective methods for diagnosing Starvation
  • Short critical sections and ReadWriteLock reduce the likelihood of starvation in high-load systems

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