Livelock (активная блокировка) — это ситуация в многопоточном программировании, когда потоки не заблокированы, но бесконечно реагируют на действия друг друга, не выполняя полезной работы. Согласно Baeldung (Java Concurrency Guide, 2024), при Livelock потоки постоянно меняют состояние в ответ на состояние соседних потоков, но ни один не достигает цели. В отличие от Deadlock, Livelock потребляет 100% CPU, что быстро разряжает батарею мобильного устройства.
Главное
Livelock (активная блокировка) — это ситуация в многопоточной системе, при которой потоки не заблокированы, но и не выполняют полезной работы. Каждый поток обнаруживает, что не может продолжить работу, и пытается это исправить, но его действия провоцируют такую же реакцию у других потоков. В результате система бесконечно переключается между состояниями, не достигая прогресса.
Классическая аналогия Livelock — два человека встречаются в узком коридоре. Каждый пытается уступить дорогу, отступая в сторону, но оба одновременно делают одно и то же движение и снова оказываются друг перед другом. Они не стоят на месте (это был бы Deadlock), а активно двигаются, но так и не расходятся. В программировании это соответствует потокам, которые постоянно освобождают и повторно захватывают ресурсы.
В мобильной разработке Livelock особенно опасен, потому что он незаметен для пользователя: приложение не зависает, интерфейс не блокируется, но аккумулятор разряжается в 2-3 раза быстрее из-за 100% загрузки CPU фоновыми потоками. По данным тестов Google (Android Battery Optimization, 2023), Livelock в фоновом Service может сократить время работы устройства от батареи на 40%.
Livelock возникает, когда несколько потоков используют одинаковую стратегию реакции на конфликт. Если Thread A не может захватить ресурс и освобождает свой текущий ресурс, а Thread B делает то же самое одновременно, оба повторяют цикл — и ситуация повторяется бесконечно. Это особенно характерно для алгоритмов с TryLock и автоматическим освобождением при неудаче.
Когда потоки используют фиксированную задержку перед повторной попыткой, они могут войти в синхронный цикл. Если оба потока ждут одинаковое время, они снова одновременно попытаются захватить ресурс и снова одновременно освободят его. Проблема решается использованием exponential backoff со случайной составляющей (jitter), как в алгоритме CSMA/CD в Ethernet.
В мобильной разработке Livelock часто возникает при неправильной реализации очередей задач. Например, когда worker thread завершает обработку сообщения, но из-за логики приоритетзации постоянно передаёт управление другому worker-у, который делает то же самое. Такие ситуации типичны для кастомных ThreadPoolExecutor-ов с нестандартной политикой RejectedExecutionHandler.
Рассмотрим ситуацию, когда два потока используют TryLock и освобождают ресурс при неудаче. Активная блокировка возникает, потому что оба потока применяют одинаковую логику и синхронно повторяют попытки.
import java.util.concurrent.locks.ReentrantLock
import java.util.concurrent.TimeUnit
class LivelockWorker(private val name: String,
private val lock1: ReentrantLock,
private val lock2: ReentrantLock) {
fun execute() {
while (true) {
if (lock1.tryLock(50, TimeUnit.MILLISECONDS)) {
if (lock2.tryLock(50, TimeUnit.MILLISECONDS)) {
println("$name — выполнено!")
lock2.unlock()
lock1.unlock()
return
} else {
lock1.unlock() // освобождаем и повторяем
}
}
Thread.sleep(50) // одинаковая задержка — ключевой фактор Livelock
}
}
}
Если два экземпляра LivelockWorker запустить с разным порядком захвата lock1 и lock2, они войдут в активную блокировку. Каждый будет захватывать первый ресурс, не получать второй, освобождать первый, ждать 50 мс и повторять — бесконечно, потребляя CPU. Исправление — добавить случайную составляющую в задержку (jitter) и ограничить число повторных попыток.
Исправленная версия использует exponential backoff со случайным jitter. После каждой неудачной попытки время ожидания увеличивается с добавлением случайного множителя, что разрушает синхронность между потоками.
fun executeWithBackoff() {
var delay = 10L
var attempts = 0
while (attempts < 5) {
if (lock1.tryLock(delay, TimeUnit.MILLISECONDS)) {
if (lock2.tryLock(delay, TimeUnit.MILLISECONDS)) {
println("Успех!")
lock2.unlock(); lock1.unlock()
return
}
lock1.unlock()
}
delay = (delay * 2 + (0..50).random())
attempts++
}
println("Не удалось после 5 попыток")
}
Несмотря на внешнюю схожесть, Livelock и Deadlock имеют принципиально разные механизмы и последствия. При Deadlock потоки заблокированы и не потребляют CPU — приложение просто зависает. При Livelock потоки активны, потребляют 100% CPU, но не выполняют полезной работы. Выбор стратегии устранения зависит от правильного определения типа блокировки.
| Параметр | Deadlock | Livelock |
|---|---|---|
| Состояние потоков | BLOCKED / WAITING | RUNNABLE |
| Потребление CPU | Минимальное | Высокое (90-100%) |
| Потребление батареи | Низкое | Высокое |
| Обнаружение | Thread Dump | CPU Profiler + визуальный анализ |
| Типичная причина | Разный порядок захвата блокировок | Одинаковая стратегия реакции на конфликт |
| Исправление | Иерархия блокировок | Retry limit + exponential backoff |
В мобильной разработке практическая разница огромна. Deadlock приводит к ANR и перезапуску приложения — его обнаруживают и сообщают через Google Play Console. Livelock остаётся незамеченным: приложение выглядит работающим, но батарея садится за час, и пользователь просто удаляет приложение. По данным Firebase Analytics (App Retention Report, 2024), 68% пользователей удаляют приложение, если оно чрезмерно расходует батарею в фоне.
Обнаружение Livelock сложнее Deadlock, поскольку система не выдаёт очевидных сигналов — нет исключений, нет ANR, нет сообщений об ошибках. Основной метод диагностики — CPU Profiler в Android Studio. Если поток постоянно в состоянии RUNNABLE, но не выполняет полезных операций ввода-вывода или вычислений — это подозрение на Livelock.
Дополнительный признак — аномальное потребление батареи при простое приложения. Android Battery Historian (инструмент из состава Android SDK) строит графики энергопотребления по компонентам. Если CPU Wakelock удерживается без видимой причины — стоит запустить Method Tracing и проанализировать стек вызовов подозрительных потоков.
На уровне кода помогает логирование повторных попыток с указанием threadId и времени. Если лог показывает тысячи повторных попыток в секунду без единого успеха — это Livelock. Рекомендуется внедрить Hystrix-подобный circuit breaker или счётчик retry с порогом срабатывания, который при превышении отключает операцию и уведомляет разработчика через Crashlytics.
Простейший и самый надёжный способ — ограничить число попыток захвата ресурса. Если после N попыток операция не удалась, поток переходит в состояние ошибки и уведомляет пользователя. N выбирается эмпирически: для мобильных приложений обычно 3-5 попыток. Это полностью исключает бесконечный Livelock ценой редких ложных срабатываний при высокой нагрузке.
Вместо фиксированной задержки между попытками используется экспоненциально растущая пауза со случайной составляющей. Формула: delay = min(baseDelay * 2^attempt, maxDelay) + random(0, jitter). Такой подход не только разрушает синхронность потоков, но и снижает общую нагрузку на систему при высокой конкуренции. Используется в алгоритмах сетевых протоколов и рекомендован Google для Firebase Realtime Database retry logic.
Назначение разных стратегий разным потокам устраняет саму причину Livelock — одинаковую реакцию на конфликт. Например, поток с высоким приоритетом получает ресурс без освобождения, а низкоприоритетный — освобождает и ждёт. В мобильной разработке UI-поток может иметь приоритет при захвате блокировок, а фоновые worker-потоки — использовать TryLock с таймаутом.
В некоторых архитектурах Livelock предотвращается на уровне дизайна: освобождение ресурсов только в одном направлении. Например, если поток A всегда передаёт управление потоку B через фиксированный канал (Channel), а B никогда не пытается вернуть управление A — цикл реакции невозможен. Pipeline-архитектура с однонаправленными стадиями обработки полностью исключает Livelock между соседними стадиями в Android CameraX и MediaPipe.
Часто задаваемые вопросы
Бесконечный цикл не зависит от внешних факторов и повторяет одну операцию без взаимодействия с другими потоками. Livelock — это всегда реакция на действия других потоков: поток меняет поведение в ответ на состояние соседних потоков, создавая замкнутую обратную связь. Thread Dump в случае Livelock показывает постоянное переключение контекста.
В базах данных Livelock возникает, когда транзакция постоянно откладывается из-за блокировок другими транзакциями. Например, СУБД использует алгоритм wait-die: если транзакция с меньшим временем старта конфликтует с более новой, она откатывается и перезапускается, но каждый раз попадает в тот же конфликт. Решается с помощью randomized restart delay.
В некоторых системах Livelock предпочтительнее Deadlock, поскольку потоки остаются активными и могут обнаружить проблему. Например, в алгоритмах оптимистичной блокировки (optimistic locking) livelock-подобное поведение допускается, если retry limit гарантирует конечное завершение. Это компромисс между производительностью и гарантией прогресса.
Livelock крайне сложно воспроизвести в тестах, поскольку он требует точного совпадения таймингов потоков. Unit-тесты выполняются детерминированно и редко выявляют активную блокировку. Рекомендуется использовать Stress Testing с многократным запуском под нагрузкой и мониторингом CPU consumption в профилировщике.
На сервере Livelock приводит к деградации производительности и timeout-ам, но сервер масштабируется горизонтально. На Android Livelock разряжает батарею и перегревает устройство, создавая худший UX. Кроме того, на мобильных устройствах ограниченное количество ядер CPU, поэтому Livelock быстрее приводит к неработоспособности всей системы.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также