Triage & Remediation of Main-Thread Lock Contention ANRs
How to deconstruct Android ANR stack traces, inspect thread wait states (SUSPENDED vs BLOCKED), and eliminate hidden UI thread deadlocks.
Anatomy of an Application Not Responding (ANR) Event
On Android platforms, an Application Not Responding (ANR) error is triggered by the system Activity Manager when the main UI thread fails to process input events within 5 seconds or broadcast receiver intents within 10 seconds (or 60 seconds for background services).
When the watchdog timer expires, Android writes an anr_traces.txt file containing the stack frames of all active threads in the process.
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 ucsCount=0 flags=1 obj=0x73b4e120 self=0x7b4a208000
| sysTid=24102 nice=-10 cgrp=default sched=0/0 handle=0x7b4ea934f8
| state=S schedstat=( 142093849 84920194 382 ) utm=10 stm=4 core=6 HZ=100
| waiting to lock <0x0a41d99b> (a com.example.storage.DatabaseLock) held by thread 14
at com.example.storage.DatabaseRepository.getUserProfile(DatabaseRepository.kt:84)
- waiting to lock <0x0a41d99b> (a com.example.storage.DatabaseLock)
at com.example.ui.profile.ProfileViewModel.loadData(ProfileViewModel.kt:42)
Finding the Culprit: Tracing held by thread
In the trace snippet above, notice the critical state indicator:
waiting to lock <0x0a41d99b> held by thread 14
The main thread is in the Blocked state waiting for a monitor lock currently owned by Thread 14. To diagnose the root cause, you must scroll down the ANR trace and inspect Thread 14:
"pool-3-thread-2" prio=5 tid=14 Native
| group="main" sCount=1 ucsCount=0 flags=1 obj=0x73b50890 self=0x7b4a259000
| sysTid=24138 nice=0 cgrp=default sched=0/0 handle=0x7b329474f8
| state=S schedstat=( 839201934 1928392 489 ) utm=62 stm=21 core=2 HZ=100
at android.database.sqlite.SQLiteConnection.nativeExecute(Native Method)
at android.database.sqlite.SQLiteConnection.execute(SQLiteConnection.java:562)
at com.example.storage.DatabaseRepository.performLargeSync(DatabaseRepository.kt:215)
- locked <0x0a41d99b> (a com.example.storage.DatabaseLock)
Here, Thread 14 holds the lock <0x0a41d99b> while executing a heavy, unbatched SQLite transaction directly on a single shared mutex. The main thread requested a quick profile read, but was queued behind a long-running sync operation, causing an ANR.
Strategic Remediation Patterns
1. Granular Lock Segregation
Avoid coarse-grained class-level or singleton synchronization. Use separate read/write locks (ReentrantReadWriteLock) or lock-free concurrency data structures (ConcurrentHashMap, atomic primitives).
2. Offloading with Coroutines and Dispatchers
Ensure that all repository operations switch context to Dispatchers.IO using structured concurrency:
suspend fun getUserProfile(userId: String): UserProfile = withContext(Dispatchers.IO) {
// Database access safely decoupled from the UI thread
database.userDao().findById(userId)
}
3. StrictMode Integration in Debug Builds
Enable Android StrictMode during continuous integration testing to immediately flag disk and network operations executed on the main thread before builds reach QA.
Conclusion
ANRs are rarely caused by the code line executing on the main thread; rather, they are caused by the lock held by background worker threads. Tracing lock ownership across thread dumps unlocks the real bottleneck.
Need Diagnostic Assistance on a Similar Issue?
Think Harbor Base specializes in isolating native stack corruption, high-impact ANRs, and memory regression. Inquire about an engineering audit for your application.