Что такое livelock?
Livelock (активная блокировка) – это разновидность взаимной блокировки, при которой потоки формально не заблокированы (их состояние не BLOCKED и не WAITING), но они не могут продвинуться дальше, потому что бесконечно реагируют на действия друг друга. В отличие от deadlock, потоки продолжают выполнять работу, но эта работа бесполезна – КПД системы падает до нуля.
Как возникает livelock
Типичный сценарий: два потока пытаются избежать deadlock, уступая друг другу ресурс, но логика уступки приводит к бесконечному циклу.
| Характеристика | Deadlock | Livelock |
|---|---|---|
| Состояние потоков | BLOCKED / WAITING |
RUNNABLE |
| Потребление CPU | Нулевое | Высокое (бесполезная работа) |
| Обнаружение JVM | Возможно через ThreadMXBean |
Автоматически не обнаруживается |
| Внешние признаки | Приложение «зависло» | Приложение «крутится», но не продвигается |
Пример livelock
Пример: два потока уступают друг другу ложку
public class LivelockExample {
static class Spoon {
private Diner owner;
Spoon(Diner owner) { this.owner = owner; }
synchronized void use() {
System.out.println(owner.name + " ест!");
}
synchronized void setOwner(Diner newOwner) {
this.owner = newOwner;
}
synchronized Diner getOwner() { return owner; }
}
static class Diner {
final String name;
boolean isHungry = true;
Diner(String name) { this.name = name; }
void eatWith(Spoon spoon, Diner partner) {
while (isHungry) {
// Если ложка не у меня — жду
if (spoon.getOwner() != this) {
try { Thread.sleep(1); } catch (InterruptedException e) { return; }
continue;
}
// Если партнёр тоже голоден — уступаю (вежливость!)
if (partner.isHungry) {
System.out.println(name + ": "
+ partner.name + " тоже голоден, уступаю ложку.");
spoon.setOwner(partner);
continue; // <-- Livelock: оба бесконечно уступают
}
// Партнёр сыт — ем
spoon.use();
isHungry = false;
spoon.setOwner(partner);
}
}
}
public static void main(String[] args) {
Diner alice = new Diner("Алиса");
Diner bob = new Diner("Боб");
Spoon spoon = new Spoon(alice);
new Thread(() -> alice.eatWith(spoon, bob)).start();
new Thread(() -> bob.eatWith(spoon, alice)).start();
// Оба потока бесконечно уступают друг другу ложку
}
}
Способы предотвращения livelock
- Случайная задержка (jitter) – добавить случайный интервал перед повторной попыткой, чтобы потоки «рассинхронизировались»:
Пример
Thread.sleep(ThreadLocalRandom.current().nextInt(10, 100)); - Приоритетная схема – ввести строгий порядок: один поток всегда имеет приоритет при конфликте.
- Ограничение числа повторов – если поток уступил N раз подряд, он прекращает уступать.
- Использование
Lock.tryLock(timeout)– вместо бесконечного цикла ставить таймаут и обрабатывать ситуацию невозможности захвата.
Аналогия из жизни. Два человека встречаются в узком коридоре. Каждый, пытаясь быть вежливым, отходит в сторону – но оба отходят в одну и ту же сторону. Они бесконечно двигаются из стороны в сторону, не продвигаясь в нужном направлении. Решение – один из них останавливается и пропускает другого.
На собеседовании. Типичный вопрос: «Чем livelock отличается от deadlock?» Ключевое отличие: при deadlock потоки заблокированы и не потребляют CPU, при livelock потоки активны (
RUNNABLE), потребляют CPU, но не совершают полезной работы. Livelock сложнее обнаружить, потому что приложение выглядит «живым». Часто livelock возникает как побочный эффект неудачной реализации предотвращения deadlock.