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 виникає, коли кілька потоків використовують однакову стратегію реакції на конфлікт. Якщо потік A не може захопити ресурс і звільняє свій поточний ресурс, а потік 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також