Documentation Index
Fetch the complete documentation index at: https://mintlify.com/AmolPardeshi99/android-performance-skills/llms.txt
Use this file to discover all available pages before exploring further.
Incorrect use of locks and mutexes is a subtle but serious source of deadlocks and ANRs in Android apps. The key mistakes — using synchronized {} with suspend calls, nested lock inversion, and Mutex re-entrance — each produce different failure modes.
synchronized {} with a suspend call — deadlock
Never place a suspend call inside a synchronized {} block. When a coroutine suspends, the coroutine dispatcher releases the thread — but the monitor lock held by synchronized {} is not released. Any other coroutine or thread waiting for the same lock is blocked indefinitely, even though no thread is actively running inside the lock.
The fix is to replace synchronized {} with Mutex, which is coroutine-aware: it suspends the coroutine (not the thread) while waiting, and releases correctly at suspension points.
// ❌ DEADLOCK: thread is released at suspension point but the monitor lock is NOT released.
// Other threads waiting on the same lock are blocked indefinitely.
val lock = Any()
suspend fun updateCache() {
synchronized(lock) {
val data = fetchFromNetwork() // suspend point — releases coroutine dispatcher, NOT the lock
cache.put(data)
}
}
// ✅ CORRECT: use Mutex (coroutine-aware lock — suspends, does not block the thread)
val mutex = Mutex()
suspend fun updateCache() {
val data = fetchFromNetwork() // suspend outside the lock
mutex.withLock { cache.put(data) }
}
Nested locks — classic deadlock
Acquiring locks in different orders across threads creates a circular wait. Thread A holds lock A and waits for lock B; Thread B holds lock B and waits for lock A. Both threads are blocked forever.
There are two correct approaches: enforce a consistent acquisition order across all code paths, or eliminate nested locks entirely by using a single-threaded dispatcher.
// ❌ DEADLOCK: Thread A acquires lockA then lockB; Thread B acquires lockB then lockA
fun threadA() { synchronized(lockA) { synchronized(lockB) { processA() } } }
fun threadB() { synchronized(lockB) { synchronized(lockA) { processB() } } }
// If A holds lockA and B holds lockB → each waits for the other → deadlock
// ✅ CORRECT option A: enforce consistent acquisition order across all code paths
// ✅ CORRECT option B: replace nested locks with a single-threaded dispatcher
val serialDispatcher = Dispatchers.IO.limitedParallelism(1)
suspend fun safeMutation() = withContext(serialDispatcher) {
processA(); processB() // only one coroutine at a time; no explicit lock needed
}
ReentrantLock without finally — lock never released
If an exception is thrown between lock() and unlock(), the lock is never released. Every thread or coroutine waiting on it is blocked permanently, eventually causing an ANR.
Always use withLock {} (Kotlin’s extension on Lock) or a try/finally block. withLock {} handles both normal and exceptional paths.
// ❌ BAD: exception between lock() and unlock() leaves lock permanently held
val lock = ReentrantLock()
fun doWork() {
lock.lock()
riskyOperation() // throws → unlock() never called → all waiters blocked forever
lock.unlock()
}
// ✅ CORRECT: always use withLock {} (Kotlin extension on Lock) or try/finally
fun doWork() {
lock.withLock { riskyOperation() } // releases even on exception
}
Mutex re-entrance — deadlock with self
Kotlin’s Mutex is not reentrant. If a coroutine that already holds a Mutex tries to acquire it again — directly or through a function call — it suspends forever waiting for itself to release it.
Refactor code so that inner functions called from within a locked section do not attempt to re-acquire the same mutex.
// ❌ DEADLOCK: Mutex is not reentrant — locking it twice on the same coroutine hangs
val mutex = Mutex()
suspend fun outer() {
mutex.withLock {
inner() // calls mutex.withLock again → deadlock
}
}
suspend fun inner() {
mutex.withLock { /* ... */ } // tries to acquire already-held mutex → suspends forever
}
// ✅ CORRECT: refactor to avoid re-entrance; use owner token for debugging
suspend fun inner() {
// called only from within outer's locked section; does not re-acquire
processInner()
}