JavaRush /Курсы /JAVA 25 SELF /Livelock и Starvation: определение, примеры

Livelock и Starvation: определение, примеры

JAVA 25 SELF
53 уровень , 1 лекция
Открыта

1. Знакомство с Livelock

Если deadlock — это когда потоки стоят и ждут друг друга вечно, то livelock (ожившая блокировка) — это когда потоки вроде бы живы, постоянно что-то делают, уступают друг другу, но... никто не продвигается вперёд! Представьте двух вежливых людей в узком коридоре: «Ой, вы проходите!» — «Нет, вы!» — «Нет, вы!» — и так до бесконечности.

Формальное определение

Livelock — ситуация, когда потоки не заблокированы, но из-за постоянных изменений своего состояния в ответ на действия других потоков не могут завершить работу. Они «живы», активно реагируют, но не выполняют полезной работы.

Как это выглядит на практике?

  • Потоки не блокируются навсегда, но застревают в бесконечном цикле уступок.
  • Система не висит, но и не делает того, что должна.

Пример из жизни

  • Два робота, которые должны разойтись в узком проходе, и оба каждый раз одновременно делают шаг в сторону друг друга — и снова мешают.
  • Два потока, которые каждый раз обнаруживают, что ресурс занят, и уступают друг другу... бесконечно.

2. Пример livelock на Java

Давайте смоделируем livelock в коде. Для простоты возьмём двух «работников», которым нужна одна ложка. В отличие от deadlock, если ложка занята, они вежливо уступают и пытаются снова — но вместе, синхронно.

Код-пример: «Вежливые работники»

public class LivelockDemo {
    static class Spoon {
        private Worker owner;

        public Spoon(Worker owner) {
            this.owner = owner;
        }

        public Worker getOwner() {
            return owner;
        }

        public synchronized void setOwner(Worker owner) {
            this.owner = owner;
        }

        public synchronized void use() {
            // Использование ложки (ничего не делает)
        }
    }

    static class Worker {
        private final String name;
        private boolean isHungry = true;

        public Worker(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public boolean isHungry() {
            return isHungry;
        }

        public void eatWith(Spoon spoon, Worker other) {
            while (isHungry) {
                // Если ложка не у меня — жду
                if (spoon.getOwner() != this) {
                    try {
                        Thread.sleep(1); // Ждём, пока ложка освободится
                    } catch (InterruptedException ignored) {}
                    continue;
                }
                // Если другой голоден — уступаю ложку
                if (other.isHungry()) {
                    System.out.println(name + ": Уступаю ложку " + other.getName());
                    spoon.setOwner(other);
                    continue;
                }
                // Ем!
                System.out.println(name + ": Я ем!");
                spoon.use();
                isHungry = false;
                System.out.println(name + ": Я наелся!");
                spoon.setOwner(other);
            }
        }
    }

    public static void main(String[] args) {
        final Worker alice = new Worker("Алиса");
        final Worker bob = new Worker("Боб");
        final Spoon spoon = new Spoon(alice);

        Thread t1 = new Thread(() -> alice.eatWith(spoon, bob));
        Thread t2 = new Thread(() -> bob.eatWith(spoon, alice));

        t1.start();
        t2.start();
    }
}

Что происходит?

  • Алиса и Боб оба голодны, ложка сначала у Алисы.
  • Алиса видит, что Боб тоже голоден, и уступает ложку.
  • Теперь ложка у Боба, но он видит, что Алиса голодна, и уступает ей.
  • Ложка «бегает» между работниками, а никто не ест — прогресса нет.

Как это выглядит в выводе?

Алиса: Уступаю ложку Боб
Боб: Уступаю ложку Алиса
Алиса: Уступаю ложку Боб
Боб: Уступаю ложку Алиса
...

Как избавиться от livelock?

Избавиться от livelock можно, если немного разбавить «вежливость» потоков. Помогает добавить случайную паузу перед повторной попыткой (например, через Thread.sleep) — тогда потоки перестанут реагировать синхронно. Также работает более «настойчивая» стратегия: если уже уступил, подожди дольше перед новой попыткой. И не перебарщивайте с джентльменством в алгоритмах — чрезмерные уступки тоже приводят к зависаниям.

3. Starvation (голодание потока)

Если livelock — это «вечная вежливость», то starvation (голодание) — это когда один или несколько потоков вообще не получают доступ к ресурсу или процессору, потому что другие всё время их опережают.

Формальное определение

Starvation — ситуация, когда поток не может получить доступ к нужному ресурсу (CPU, памяти, блокировке), потому что другие потоки постоянно его опережают. В результате «голодающий» поток либо выполняется крайне редко, либо вообще не выполняется.

Причины starvation

  • Несправедливые блокировки. Например, обычный synchronized-блок не гарантирует, что поток, который дольше всех ждёт, зайдёт первым.
  • Приоритеты потоков. Если потоки с высоким приоритетом постоянно занимают процессор, низкоприоритетные могут «умирать с голоду» (setPriority).
  • Бесконечные циклы в других потоках. Если кто-то не уступает CPU (не вызывает Thread.sleep или Thread.yield()), другие потоки могут не получить время выполнения.

4. Пример starvation на Java

Пример: Поток с низким приоритетом не выполняется

public class StarvationDemo {
    public static void main(String[] args) {
        Runnable highPriorityTask = () -> {
            while (true) {
                // Интенсивная работа, не уступает CPU
            }
        };

        Runnable lowPriorityTask = () -> {
            while (true) {
                System.out.println("Я низкоприоритетный поток!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ignored) {}
            }
        };

        Thread high1 = new Thread(highPriorityTask);
        Thread high2 = new Thread(highPriorityTask);
        Thread low = new Thread(lowPriorityTask);

        high1.setPriority(Thread.MAX_PRIORITY); // 10
        high2.setPriority(Thread.MAX_PRIORITY); // 10
        low.setPriority(Thread.MIN_PRIORITY);   // 1

        high1.start();
        high2.start();
        low.start();
    }
}

Как это проявляется?

  • Потоки с высоким приоритетом всё время заняты работой, не уступают CPU.
  • Поток с низким приоритетом почти не выполняется (или не выполняется вовсе).
  • На современных JVM/ОС приоритеты могут сглаживаться планировщиком, но на некоторых системах голодание заметно.

Ещё один пример: starvation из-за несправедливой блокировки

public class StarvationLockDemo {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        // 5 потоков, которые всё время захватывают lock
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                while (true) {
                    synchronized (lock) {
                        // Занимаем lock надолго
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException ignored) {}
                    }
                }
            }).start();
        }

        // Один голодающий поток
        new Thread(() -> {
            while (true) {
                synchronized (lock) {
                    System.out.println("Голодающий поток получил lock!");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException ignored) {}
                }
            }
        }).start();
    }
}

В этом примере «голодающий» поток может очень долго не получить доступ к lock, если другие потоки постоянно его занимают.

5. Как обнаружить и предотвратить livelock и starvation

Как обнаружить?

  • Livelock: программа работает, потоки не висят, но нет прогресса (нет результата, нет выхода из циклов).
  • Starvation: некоторые потоки почти не выполняются (редкие сообщения в логах или их отсутствие).

Инструменты

  • Логирование: помечайте начало/конец работы, захват/освобождение ресурсов.
  • Мониторинг: VisualVM, Java Mission Control — смотрите, какие потоки активны и чем заняты.
  • Thread dump: проверьте, не застряли ли потоки в ожидании lock.

Как избежать?

Для livelock:

  • Не делайте слишком «вежливых» уступок — добавляйте небольшую случайную задержку перед повторной попыткой (Thread.sleep).
  • Вводите случайность в порядок повторных попыток, чтобы уйти от синхронного поведения потоков.
  • Используйте неблокирующие структуры/алгоритмы (атомарные переменные, CAS-подход).

Для starvation:

  • Используйте «справедливые» блокировки. Например, ReentrantLock с fairness:
java.util.concurrent.locks.ReentrantLock lock = new java.util.concurrent.locks.ReentrantLock(true); // справедливый режим
  • Не злоупотребляйте приоритетами потоков — чаще оставляйте приоритет по умолчанию.
  • Минимизируйте время внутри критических секций (synchronized/Lock).
  • Используйте очереди задач, где обслуживание близко к FIFO.

Таблица: Deadlock, Livelock, Starvation — сравнение

Проблема Что происходит Потоки «живы»? Прогресс? Типичный симптом
Deadlock Все ждут друг друга Нет Нет Программа «зависла»
Livelock Все уступают, но не двигаются Да Нет Потоки работают, но результата нет
Starvation Одни работают, другие почти нет Да (часть) Частично Некоторые потоки «голодают»

Аналогии и интересные факты

  • Livelock — как двое, которые одновременно делают шаг влево, чтобы разойтись, и снова сталкиваются.
  • Starvation — как очередь в магазине, где кассир обслуживает только «своих», а остальные стоят вечно.

Интересный факт: livelock встречается реже, чем deadlock, но обнаружить его сложнее — программа «не висит», а что-то делает!

6. Типичные ошибки при работе с livelock и starvation

Ошибка №1: «Вежливые уступки» без задержки. Если потоки слишком часто уступают друг другу без паузы, они могут попасть в livelock. Добавляйте небольшую случайную задержку перед повторной попыткой захвата ресурса (Thread.sleep).

Ошибка №2: Ожидание только на synchronized, без справедливых блокировок. При большом количестве потоков обычный synchronized не гарантирует, что «самый голодающий» получит доступ. Используйте ReentrantLock с fairness, если это критично.

Ошибка №3: Злоупотребление приоритетами потоков. Попытка «ускорить» важные потоки через setPriority часто приводит к starvation других. Не трогайте приоритеты без реальной необходимости.

Ошибка №4: Отсутствие мониторинга и логирования. Livelock и starvation сложно заметить без логов: программа «работает», но результата нет. Логируйте ключевые события и используйте профайлеры/дампы потоков.

Ошибка №5: Слишком долгие критические секции. Если поток долго держит lock, остальные будут ждать (или «голодать»). Минимизируйте время внутри synchronized/Lock-блоков.

1
Задача
JAVA 25 SELF, 53 уровень, 1 лекция
Недоступна
Перекрёсток вежливости: Моделирование "livelock" 🚶‍♂️🚶‍♀️
Перекрёсток вежливости: Моделирование "livelock" 🚶‍♂️🚶‍♀️
1
Задача
JAVA 25 SELF, 53 уровень, 1 лекция
Недоступна
Бедный Фоновый Отчёт: Пример "starvation" из-за приоритетов 📉
Бедный Фоновый Отчёт: Пример "starvation" из-за приоритетов 📉
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ