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 (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.
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.
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.
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.
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.
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.
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()
}
}
}
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.
| Parameter | Starvation | Deadlock | Livelock |
|---|---|---|---|
| Thread State | RUNNABLE | BLOCKED | RUNNABLE |
| Progress | None | None | None (though active) |
| CPU Usage | Low | Minimal | High (up to 100%) |
| Cause | Unfair scheduling | Cyclic waiting | Same response to conflict |
| Main Fix | Fair Lock, shorten critical sections | Lock hierarchy | Retry 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.
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.
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 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.
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.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
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.
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.
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.
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.
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
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.
Read also