Deadlock is a state in which two or more threads indefinitely wait for the release of resources held by other participants. According to Oracle Java Tutorials (2024), a Deadlock occurs in circular waiting when each thread holds a lock needed by another thread. Without special detection tools, Deadlock completely stops application execution without visible errors.
Key Takeaways
Deadlock is a situation in multithreaded programming where two or more threads permanently block each other. Each thread holds a resource needed by another thread and does not release it while waiting to acquire the missing resource. As a result, none of the threads can continue execution.
In mobile development, Deadlock is especially critical because it does not throw exceptions or cause crashes. The application simply stops responding to user actions (ANR — Application Not Responding), and the only way out is to forcibly terminate the process. According to Google (Android Performance Patterns, 2023), about 15% of ANR reports in Google Play Console are related to mutual blocking in background threads.
The key difference between Deadlock and other concurrency problems is its irreversibility without external intervention. Threads will not release resources on their own because the operating system scheduler cannot forcibly revoke a lock. This distinguishes Deadlock from Livelock, where threads are active but not doing useful work.
In 1971, Edward G. Coffman formulated four mandatory conditions necessary for a Deadlock to occur. If at least one of them is absent, mutual blocking is impossible. These conditions are known as Coffman's conditions and form the basis of all Deadlock prevention algorithms.
A resource can be acquired by only one thread at any given time. If a resource allows simultaneous reading by multiple threads (e.g., ReadWriteLock in read mode), Deadlock does not occur. This condition stems from the very nature of Mutex and locks.
A thread holds an already acquired resource and simultaneously waits to acquire another resource. If a thread can release the current resource before requesting the next one (via two-phase locking), the Hold and Wait condition is broken. In Android, this often manifests when a thread holds a database lock and tries to acquire a SharedPreferences lock.
The operating system cannot forcibly take away a lock from a thread. The resource is released only when the thread itself releases it. In some systems (e.g., SQLite WAL mode), forced preemption is implemented at the level of individual operations, which reduces the risk of Deadlock.
There is a closed chain of threads, each of which waits for a resource held by the next in the chain. For example, thread A holds resource 1 and waits for resource 2, thread B holds resource 2 and waits for resource 1. This is the only condition a developer can eliminate architecturally — through a lock hierarchy. If all threads acquire resources in a strictly defined global order, a cycle is physically impossible.
In practice, in Android applications, Deadlock most often occurs due to implicit intersection of locks at different levels: database lock (Room), SharedPreferences lock, and in-memory collection lock. Each of these locks is managed by different components, and without a centralized protocol for acquisition order, developers unintentionally create cycles.
Let's consider a classic example of mutual blocking — two threads acquire locks in a different order. If the first thread locks resource A and tries to acquire B, and the second locks B and tries to acquire A, a Deadlock occurs.
class DeadlockExample {
private val lockA = Any()
private val lockB = Any()
fun operationA() {
synchronized(lockA) {
Thread.sleep(50) // work simulation
synchronized(lockB) {
println("operationA completed")
}
}
}
fun operationB() {
synchronized(lockB) { // reverse order!
Thread.sleep(50)
synchronized(lockA) {
println("operationB completed")
}
}
}
}
fun main() {
val ex = DeadlockExample()
Thread { ex.operationA() }.start()
Thread { ex.operationB() }.start()
// Application will hang forever — Deadlock!
}
In this example, operationA acquires lockA, and operationB acquires lockB. Then each tries to acquire the second lock — and both wait indefinitely. The program hangs without an error message. The only way to fix this is to guarantee the same lock acquisition order across all methods.
These three concurrency problems are often confused, but their mechanisms and consequences are fundamentally different. Deadlock — complete halt, Starvation — infinite waiting for a resource, Livelock — active idleness. Understanding the differences is critically important for choosing the right resolution strategy.
| Characteristic | Deadlock | Starvation | Livelock |
|---|---|---|---|
| Thread state | Blocked (BLOCKED) | Ready (RUNNABLE) | Active (RUNNABLE) |
| Work being done | No | No | Yes, but useless |
| Cause | Circular waiting | Unfair scheduling | Incorrect conflict handling |
| Detection | Thread Dump, timeouts | Progress monitoring | Retry counter |
Starvation occurs when the scheduler constantly postpones the execution of a low-priority thread in favor of others. Unlike Deadlock, the thread is not blocked — it is ready to run but does not get CPU time. In Android, a typical scenario is a low-priority background thread that never executes if the UI thread and Service threads are constantly active.
Livelock is a situation where threads are not blocked but endlessly react to each other's actions without performing useful work. The classic analogy is two people meeting in a hallway and both trying to step aside, moving in the same direction. Unlike Deadlock, threads in Livelock consume CPU, draining the device battery.
Thread Dump is the primary tool for detecting mutual blocking in JVM and Android Runtime. During a dump, the JVM automatically analyzes the dependency graph between monitors and marks Deadlock cycles. In Android Studio, thread dumps can be obtained via Android Profiler or the kill -3 PID command from ADB Shell.
Automatic Deadlock detection at runtime is implemented through Watchdog timers. If a thread does not complete an operation within a specified timeout, the watchdog initiates a dump and sends a report to the Crash Reporting system (Firebase Crashlytics, Sentry). According to Sentry (Issue Resolution Report, 2024), configuring a watchdog reduces Deadlock diagnosis time from weeks to a few hours.
During development, JetBrains ThreadSafe static analyzer and the Checker Framework with the Lock Checker module are effective. These tools analyze the order of lock acquisition at the source code level and warn about potential cycles. Additionally, Test-Driven Deadlock Detection is recommended — stress tests that run operations with different lock orders across hundreds of threads.
Special attention deserves Cooperative Deadlock Detection — a method where threads exchange information about acquired locks through a global registry. If a thread detects a potential cycle, it releases all resources and retries the operation. This approach is used in distributed systems (Apache ZooKeeper, Google Chubby) and is gradually being adopted in mobile development through libraries like Jetpack Sync.
The most reliable way is to establish a global lock acquisition order across the entire application. If all threads always acquire the lock with a lower number first, and then the one with a higher number, circular waiting (the Circular Wait condition) is impossible. In large projects, the order is documented and verified through code review.
TryLock is a locking method that does not block a thread indefinitely but returns false if the lock is not acquired within a specified time. In Java, this is implemented via ReentrantLock.tryLock(timeout, TimeUnit), in Kotlin Coroutines — via Mutex.withLock with a timeout. On failure, the thread releases all acquired resources and retries later.
Banker's Algorithm is a theoretical Deadlock prevention method proposed by Edsger Dijkstra. It models resource allocation as bank transactions: the system does not allocate a resource if it could lead to an unsafe state (deadlock). In practice, the algorithm is rarely used in mobile development due to the difficulty of knowing threads' maximum needs in advance, but its principles are used in SQLite databases and file systems.
Frequently Asked Questions
No, mutual blocking requires at least two threads. In single-threaded code, all operations execute sequentially, so circular waiting is impossible. However, Deadlock can occur between processes when using file locks or interprocess semaphores.
In coroutines, Deadlock occurs at the level of suspend functions and does not block the OS thread, making it less noticeable. Mutex from kotlinx.coroutines is a suspending lock — it does not block the thread, but the coroutine does not execute. For detection, use DebugProbes from the kotlinx-coroutines-debug module.
SQLite Deadlock occurs when two database connections try to execute transactions in different orders. SQLite detects such situations and returns the SQLITE_BUSY or SQLITE_LOCKED error code. In Android, it is recommended to use Room with a single database instance and transactions via @Transaction, which eliminates inter-connection Deadlock.
Android Runtime has a built-in Deadlock detector that runs when an ANR (Application Not Responding) is generated. The system analyzes the Thread Dump of all application threads and marks mutual blocking. The result is available at /data/anr/traces.txt and in Google Play Console under ANR Reports.
First, obtain a Thread Dump of all application threads. Analyze which locks each thread holds and which it is trying to acquire. Implement a Watchdog timer with automatic dump when the time limit is exceeded. After fixing, add the ThreadSafety lint rule to your CI pipeline to prevent recurrence.
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