Deadlock (взаимная блокировка) — это состояние, в котором два или более потоков бесконечно ожидают освобождения ресурсов, захваченных другими участниками. Согласно Oracle Java Tutorials (2024), Deadlock возникает при круговом ожидании, когда каждый поток держит блокировку, необходимую другому потоку. Без специальных средств детекции Deadlock полностью останавливает выполнение приложения без видимых ошибок.
Главное
Deadlock (взаимная блокировка) — это ситуация в многопоточном программировании, при которой два или более потоков навсегда блокируют друг друга. Каждый поток удерживает ресурс, необходимый другому потоку, и не освобождает его, ожидая захвата недостающего ресурса. В результате ни один из потоков не может продолжить выполнение.
В мобильной разработке Deadlock особенно критичен, потому что он не вызывает исключений или крашей. Приложение просто перестаёт отвечать на действия пользователя (ANR — Application Not Responding), и единственный выход — принудительное завершение процесса. По данным Google (Android Performance Patterns, 2023), около 15% ANR-отчётов в Google Play Console связаны с взаимными блокировками в фоновых потоках.
Ключевое отличие Deadlock от других проблем конкурентности — его необратимость без внешнего вмешательства. Потоки не освободят ресурсы самостоятельно, поскольку планировщик операционной системы не может принудительно отозвать блокировку. Это отличает Deadlock от Livelock, где потоки активны, но не выполняют полезной работы.
В 1971 году Edward G. Coffman сформулировал четыре обязательных условия, необходимых для возникновения Deadlock. Если хотя бы одно из них отсутствует, взаимная блокировка невозможна. Эти условия известны как условия Коффмана и лежат в основе всех алгоритмов предотвращения Deadlock.
Ресурс может быть захвачен только одним потоком в каждый момент времени. Если ресурс допускает одновременное чтение несколькими потоками (например, ReadWriteLock в режиме чтения), Deadlock не возникает. Это условие вытекает из самой природы Mutex и блокировок.
Поток удерживает уже захваченный ресурс и одновременно ожидает захвата другого ресурса. Если поток может освободить текущий ресурс перед запросом следующего (через two-phase locking), условие Hold and Wait нарушается. В Android это часто проявляется, когда поток держит блокировку БД и пытается захватить блокировку SharedPreferences.
Операционная система не может принудительно отобрать блокировку у потока. Ресурс освобождается только когда поток сам его отпускает. В некоторых системах (например, SQLite WAL mode) реализовано принудительное вытеснение на уровне отдельных операций, что снижает риск Deadlock.
Существует замкнутая цепь потоков, каждый из которых ожидает ресурса, удерживаемого следующим в цепи. Например, поток A держит ресурс 1 и ждёт ресурс 2, поток B держит ресурс 2 и ждёт ресурс 1. Это единственное условие, которое разработчик может устранить архитектурно — через иерархию блокировок. Если все потоки захватывают ресурсы в строго заданном глобальном порядке, цикл физически невозможен.
На практике в Android-приложениях Deadlock чаще всего возникает из-за неявного пересечения блокировок разных уровней: блокировка БД (Room), блокировка SharedPreferences и блокировка коллекции в памяти. Каждая из этих блокировок управляется разными компонентами, и без централизованного протокола порядка захвата разработчики непреднамеренно создают циклы.
Рассмотрим классический пример взаимной блокировки — два потока захватывают блокировки в разном порядке. Если первый поток блокирует ресурс A и пытается захватить B, а второй — блокирует B и пытается захватить A, возникает Deadlock.
class DeadlockExample {
private val lockA = Any()
private val lockB = Any()
fun operationA() {
synchronized(lockA) {
Thread.sleep(50) // симуляция работы
synchronized(lockB) {
println("operationA выполнена")
}
}
}
fun operationB() {
synchronized(lockB) { // обратный порядок!
Thread.sleep(50)
synchronized(lockA) {
println("operationB выполнена")
}
}
}
}
fun main() {
val ex = DeadlockExample()
Thread { ex.operationA() }.start()
Thread { ex.operationB() }.start()
// Приложение зависнет навсегда — Deadlock!
}
В этом примере operationA захватывает lockA, а operationB захватывает lockB. Затем каждый пытается захватить вторую блокировку — и оба бесконечно ждут. Программа зависает без сообщения об ошибке. Единственный способ исправления — гарантировать одинаковый порядок захвата блокировок во всех методах.
Эти три проблемы конкурентности часто путают, но их механизмы и последствия принципиально различны. Deadlock — полная остановка, Starvation — бесконечное ожидание ресурса, Livelock — активное бездействие. Понимание различий критически важно для выбора правильной стратегии устранения.
| Характеристика | Deadlock | Starvation | Livelock |
|---|---|---|---|
| Состояние потоков | Заблокированы (BLOCKED) | Готовы (RUNNABLE) | Активны (RUNNABLE) |
| Выполнение работы | Нет | Нет | Есть, но бесполезное |
| Причина | Циклическое ожидание | Несправедливое планирование | Некорректная обработка конфликта |
| Обнаружение | Thread Dump, таймауты | Мониторинг прогресса | Счётчик повторных попыток |
Starvation (голодание) возникает, когда планировщик постоянно откладывает выполнение низкоприоритетного потока в пользу других. В отличие от Deadlock, поток не заблокирован — он готов к выполнению, но не получает процессорного времени. В Android типичный сценарий — фоновый поток с низким приоритетом никогда не выполняется, если UI-поток и Service-потоки постоянно активны.
Livelock (активная блокировка) — ситуация, при которой потоки не заблокированы, но бесконечно реагируют на действия друг друга, не выполняя полезной работы. Классическая аналогия — два человека встречаются в коридоре и оба пытаются уступить дорогу, двигаясь в одну и ту же сторону. В отличие от Deadlock, потоки в Livelock потребляют CPU, разряжая батарею устройства.
Thread Dump (дамп потоков) — основной инструмент обнаружения взаимных блокировок в JVM и Android Runtime. При дампе JVM автоматически анализирует граф зависимостей между мониторами и помечает Deadlock-циклы. В Android Studio дамп потоков можно получить через Android Profiler или команду kill -3 PID из ADB Shell.
Автоматическое обнаружение Deadlock на этапе выполнения реализуется через Watchdog-таймеры. Если поток не завершает операцию за заданный таймаут, watchdog инициирует снятие дампа и отправляет отчёт в Crash Reporting-систему (Firebase Crashlytics, Sentry). По данным Sentry (Issue Resolution Report, 2024), настройка watchdog сокращает время диагностики Deadlock с недель до нескольких часов.
На этапе разработки эффективны статический анализатор ThreadSafe от JetBrains и Checker Framework с модулем Lock Checker. Эти инструменты анализируют порядок захвата блокировок на уровне исходного кода и предупреждают о потенциальных циклах. Дополнительно рекомендуется использовать Test-Driven Deadlock Detection — стресс-тесты, которые запускают операции с разными порядками блокировок в сотнях потоков.
Отдельного внимания заслуживает Cooperative Deadlock Detection — метод, при котором потоки обмениваются информацией о захваченных блокировках через глобальный registry. Если поток обнаруживает потенциальный цикл, он освобождает все ресурсы и повторяет операцию. Этот подход используется в распределённых системах (Apache ZooKeeper, Google Chubby) и постепенно внедряется в мобильную разработку через библиотеки типа Jetpack Sync.
Самый надёжный способ — установить глобальный порядок захвата блокировок во всём приложении. Если все потоки всегда сначала захватывают блокировку с меньшим номером, а затем с большим, циклическое ожидание (условие Circular Wait) невозможно. В крупных проектах порядок фиксируется в документации и проверяется code review.
TryLock — метод блокировки, который не блокирует поток бесконечно, а возвращает false, если блокировка не получена за заданное время. В Java это реализовано через ReentrantLock.tryLock(timeout, TimeUnit), в Kotlin Coroutines — через Mutex.withLock с таймаутом. При неудаче поток освобождает все захваченные ресурсы и повторяет попытку позже.
Алгоритм банкира — теоретический метод предотвращения Deadlock, предложенный Edsger Dijkstra. Он моделирует распределение ресурсов как банковские транзакции: система не выделяет ресурс, если это может привести к небезопасному состоянию (deadlock). На практике алгоритм редко применяется в мобильной разработке из-за сложности предварительного знания максимальных потребностей потоков, но его принципы используются в БД SQLite и файловых системах.
Часто задаваемые вопросы
Нет, для взаимной блокировки необходимо минимум два потока. В однопоточном коде все операции выполняются последовательно, поэтому циклическое ожидание невозможно. Однако Deadlock может возникнуть между процессами при использовании файловых блокировок или межпроцессных семафоров.
В корутинах Deadlock возникает на уровне приостановленных функций (suspend) и не блокирует поток ОС, что делает его менее заметным. Mutex из kotlinx.coroutines — приостанавливающий (suspending), он не блокирует поток, но корутина при этом не выполняется. Для обнаружения используйте DebugProbes из модуля kotlinx-coroutines-debug.
SQLite Deadlock возникает, когда два соединения с БД пытаются выполнить транзакции в разных порядках. SQLite обнаруживает такие ситуации и возвращает код ошибки SQLITE_BUSY или SQLITE_LOCKED. В Android рекомендуется использовать Room с единственным экземпляром БД и транзакциями через @Transaction, что устраняет межсоединительный Deadlock.
Android Runtime имеет встроенный детектор Deadlock, который запускается при генерации ANR (Application Not Responding). Система анализирует Thread Dump всех потоков приложения и помечает взаимные блокировки. Результат доступен в /data/anr/traces.txt и Google Play Console в разделе ANR Reports.
Первым делом получите Thread Dump всех потоков приложения. Проанализируйте, какие блокировки удерживает каждый поток и какие пытается захватить. Внедрите Watchdog-таймер с автоматическим дампом при превышении лимита времени. После исправления добавьте lint-правило ThreadSafety в CI-пайплайн для предотвращения рецидивов.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также