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.
GO TO FULL VERSION