CodeGym /Cursos /JAVA 25 SELF /Livelock e Starvation: definição, exemplos

Livelock e Starvation: definição, exemplos

JAVA 25 SELF
Nível 53, Lição 1
Disponível

1. Introdução ao livelock

Se deadlock é quando as threads ficam paradas esperando umas às outras para sempre, então livelock (bloqueio vivo) é quando as threads parecem vivas, estão sempre fazendo algo, cedem uma à outra, mas... ninguém avança! Imagine duas pessoas educadas em um corredor estreito: “Ah, pode passar!” — “Não, você!” — “Não, você!” — e assim por diante, infinitamente.

Definição formal

Livelock — situação em que as threads não estão bloqueadas, mas, devido a mudanças constantes de estado em resposta às ações de outras threads, não conseguem finalizar o trabalho. Elas estão “vivas”, reagem ativamente, mas não executam trabalho útil.

Como isso se parece na prática?

  • As threads não ficam bloqueadas para sempre, mas ficam presas em um ciclo infinito de concessões.
  • O sistema não trava, mas também não faz o que deveria.

Exemplo do mundo real

  • Dois robôs que precisam se cruzar em um corredor estreito e, toda vez, ambos dão um passo para o lado ao mesmo tempo — e voltam a se atrapalhar.
  • Duas threads que toda vez percebem que o recurso está ocupado e cedem uma à outra... indefinidamente.

2. Exemplo de livelock em Java

Vamos simular um livelock no código. Para simplificar, vamos usar dois “trabalhadores” que precisam de uma única colher. Diferente do deadlock, se a colher estiver ocupada, eles cedem educadamente e tentam de novo — mas juntos, de forma síncrona.

Exemplo de código: “Trabalhadores educados”

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() {
            // Uso da colher (não faz nada)
        }
    }

    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) {
                // Se a colher não está comigo — espero
                if (spoon.getOwner() != this) {
                    try {
                        Thread.sleep(1); // Espera até a colher ficar livre
                    } catch (InterruptedException ignored) {}
                    continue;
                }
                // Se o outro está com fome — cedo a colher
                if (other.isHungry()) {
                    System.out.println(name + ": Cedo a colher para " + other.getName());
                    spoon.setOwner(other);
                    continue;
                }
                // Comer!
                System.out.println(name + ": Estou comendo!");
                spoon.use();
                isHungry = false;
                System.out.println(name + ": Estou satisfeito!");
                spoon.setOwner(other);
            }
        }
    }

    public static void main(String[] args) {
        final Worker alice = new Worker("Alice");
        final Worker bob = new Worker("Bob");
        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();
    }
}

O que acontece?

  • Alice e Bob estão com fome; a colher começa com Alice.
  • Alice vê que Bob também está com fome e cede a colher.
  • Agora a colher está com Bob, mas ele vê que Alice está com fome e cede a ela.
  • A colher “corre” entre os trabalhadores e ninguém come — não há progresso.

Como isso aparece na saída?

Alice: Cedo a colher para Bob
Bob: Cedo a colher para Alice
Alice: Cedo a colher para Bob
Bob: Cedo a colher para Alice
...

Como se livrar do livelock?

É possível eliminar o livelock se reduzirmos um pouco a “educação” das threads. Ajuda adicionar uma pausa aleatória antes de uma nova tentativa (por exemplo, via Thread.sleep) — assim as threads deixam de reagir de forma síncrona. Também funciona uma estratégia mais “persistente”: se você já cedeu, espere mais antes de tentar novamente. E não exagere na gentileza dos algoritmos — concessões excessivas também levam a travamentos.

3. Starvation (fome de recursos da thread)

Se livelock é “educação eterna”, então starvation (fome) é quando uma ou mais threads simplesmente não obtêm acesso a um recurso ou à CPU porque outras sempre as ultrapassam.

Definição formal

Starvation — situação em que uma thread não consegue obter acesso ao recurso necessário (CPU, memória, bloqueio), porque outras threads constantemente a ultrapassam. Como resultado, a thread “faminta” ou executa muito raramente, ou nem executa.

Causas de starvation

  • Bloqueios injustos. Por exemplo, um bloco synchronized comum não garante que a thread que espera há mais tempo entrará primeiro.
  • Prioridades de threads. Se threads de alta prioridade ocupam a CPU o tempo todo, as de baixa prioridade podem “passar fome” (setPriority).
  • Loops infinitos em outras threads. Se alguém não cede a CPU (não chama Thread.sleep ou Thread.yield()), outras threads podem não receber tempo de execução.

4. Exemplo de starvation em Java

Exemplo: uma thread de baixa prioridade não executa

public class StarvationDemo {
    public static void main(String[] args) {
        Runnable highPriorityTask = () -> {
            while (true) {
                // Trabalho intensivo, não cede a CPU
            }
        };

        Runnable lowPriorityTask = () -> {
            while (true) {
                System.out.println("Sou uma thread de baixa prioridade!");
                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();
    }
}

Como isso se manifesta?

  • As threads de alta prioridade estão ocupadas o tempo todo e não cedem a CPU.
  • A thread de baixa prioridade quase não executa (ou não executa).
  • Em JVM/SO modernos, o agendador pode suavizar as prioridades, mas em alguns sistemas a fome é perceptível.

Outro exemplo: starvation devido a bloqueio injusto

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

    public static void main(String[] args) {
        // 5 threads que capturam o lock o tempo todo
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                while (true) {
                    synchronized (lock) {
                        // Mantemos o lock por muito tempo
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException ignored) {}
                    }
                }
            }).start();
        }

        // Uma thread faminta
        new Thread(() -> {
            while (true) {
                synchronized (lock) {
                    System.out.println("A thread faminta obteve o lock!");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException ignored) {}
                }
            }
        }).start();
    }
}

Neste exemplo, a thread “faminta” pode demorar muito para obter acesso ao lock se outras threads o ocuparem continuamente.

5. Como detectar e prevenir livelock e starvation

Como detectar?

  • Livelock: o programa roda, as threads não ficam penduradas, mas não há progresso (sem resultado, sem saída de loops).
  • Starvation: algumas threads quase não executam (mensagens raras nos logs ou ausência delas).

Ferramentas

  • Registro em log: marque início/fim do trabalho, aquisição/liberação de recursos.
  • Monitoramento: VisualVM, Java Mission Control — veja quais threads estão ativas e em que estão trabalhando.
  • Thread dump: verifique se as threads não estão presas aguardando um lock.

Como evitar?

Para livelock:

  • Não faça concessões “educadas” demais — adicione um pequeno atraso aleatório antes de nova tentativa (Thread.sleep).
  • Introduza aleatoriedade na ordem das novas tentativas para evitar comportamento síncrono das threads.
  • Use estruturas/algoritmos não bloqueantes (variáveis atômicas, abordagem CAS).

Para starvation:

  • Use bloqueios “justos”. Por exemplo, ReentrantLock com fairness:
java.util.concurrent.locks.ReentrantLock lock = new java.util.concurrent.locks.ReentrantLock(true); // modo justo
  • Não abuse das prioridades das threads — na maioria dos casos, deixe a prioridade padrão.
  • Minimize o tempo dentro de seções críticas (synchronized/Lock).
  • Use filas de tarefas cujo atendimento seja próximo de FIFO.

Tabela: Deadlock, Livelock, Starvation — comparação

Problema O que acontece Threads “vivas”? Progresso? Sintoma típico
Deadlock Todos esperam uns aos outros Não Não O programa “congelou”
Livelock Todos cedem, mas não avançam Sim Não As threads trabalham, mas não há resultado
Starvation Algumas trabalham, outras quase não Sim (parcial) Parcialmente Algumas threads “passam fome”

Analogias e fatos interessantes

  • Livelock — como duas pessoas que, ao mesmo tempo, dão um passo para a esquerda para se desviar e acabam se esbarrando novamente.
  • Starvation — como uma fila no mercado em que o caixa atende apenas “os seus”, e os demais ficam esperando para sempre.

Fato interessante: livelock ocorre com menos frequência do que deadlock, mas é mais difícil de detectar — o programa “não congela”, mas está fazendo algo!

6. Erros comuns ao lidar com livelock e starvation

Erro nº 1: “Concessões educadas” sem atraso. Se as threads cedem com muita frequência sem pausa, podem cair em livelock. Adicione um pequeno atraso aleatório antes de tentar capturar o recurso novamente (Thread.sleep).

Erro nº 2: Esperar apenas com synchronized, sem bloqueios justos. Com muitas threads, synchronized comum não garante que a “mais faminta” terá acesso. Use ReentrantLock com fairness quando isso for crítico.

Erro nº 3: Abusar das prioridades das threads. Tentar “acelerar” threads importantes via setPriority frequentemente leva à starvation das demais. Não mude prioridades sem necessidade real.

Erro nº 4: Falta de monitoramento e logs. Livelock e starvation são difíceis de perceber sem logs: o programa “funciona”, mas não há resultado. Registre eventos chave e use profileadores/dumps de threads.

Erro nº 5: Seções críticas longas demais. Se uma thread mantém um lock por muito tempo, as outras vão esperar (ou “passar fome”). Minimize o tempo dentro de blocos synchronized/Lock.

1
Tarefa
JAVA 25 SELF, nível 53, lição 1
Bloqueado
Cruzamento da cortesia: Simulação "livelock" 🚶‍♂️🚶‍♀️
Cruzamento da cortesia: Simulação "livelock" 🚶‍♂️🚶‍♀️
1
Tarefa
JAVA 25 SELF, nível 53, lição 1
Bloqueado
Pobre Relatório em Segundo Plano: Exemplo de "starvation" devido às prioridades 📉
Pobre Relatório em Segundo Plano: Exemplo de "starvation" devido às prioridades 📉
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION