A regra que deve sobreviver à intercalação
Começa pela invariante. Num contador de unidades disponíveis, cada reserva bem-sucedida deve retirar uma unidade e o valor nunca deve ser negativo. Duas chamadas podem observar o mesmo valor antes de qualquer uma o alterar. Se ambas lerem um e depois decrementarem, há duas reservas para uma só unidade. O problema existe mesmo que cada leitura e cada decremento sejam atómicos isoladamente.
volatile resolve certas relações de visibilidade e ordenação, mas não transforma count++ numa única atualização. A expressão contém leitura, cálculo e escrita. Duas threads podem ler zero e ambas escrever um. Tornar a referência final também não torna o objeto referido imutável ou a operação composta atómica.
Escolhe uma fronteira que proteja toda a decisão: um lock comum à condição e à alteração, ou uma atualização condicional atómica que revalide o estado. Evita diagnosticar apenas pela frequência observada. Uma execução sem perda de dados não prova que todas as intercalações permitidas conservem a invariante.
O monitor tem uma identidade
Um método synchronized de instância usa o monitor dessa instância. Se o estado é static e duas instâncias alteram esse estado usando os seus próprios monitores, não existe exclusão comum entre as chamadas. O modificador está presente, mas a proteção não corresponde ao recurso. Todos os caminhos de acesso envolvidos têm de obedecer à mesma política.
Quando há vários locks, a ordem de aquisição faz parte do desenho. Se um participante retém A e espera por B, enquanto outro retém B e espera por A, nenhum pode avançar. Uma ordem global, aplicada em todos os caminhos relevantes, elimina esse ciclo particular. Não prova ausência de todos os bloqueios possíveis noutras dependências.
Acrescentar sleep não liberta o monitor nem cria uma relação de sincronização que corrija acessos mal protegidos. Pode apenas mudar a probabilidade de observar o problema. Na análise de um incidente, identifica o objeto bloqueado, quem o detém, quem espera e que trabalho permitiria libertá-lo. Um nome de variável igual não garante que dois caminhos usam o mesmo objeto.
Esperar por uma condição
Num protocolo baseado em monitor, wait liberta o monitor correspondente enquanto a thread espera e volta a adquiri-lo antes do retorno normal. Isso permite que um produtor entre, altere o estado e notifique. A condição tem de ser verificada sob o mesmo lock usado para a alterar.
Usa um ciclo em torno de wait. Uma thread pode acordar sem a notificação que esperava, ou outro consumidor pode alterar a condição antes de ela voltar a adquirir o monitor. A notificação indica que vale a pena reavaliar; não concede automaticamente o recurso. notifyAll acorda participantes, mas eles continuam a disputar o monitor e a avaliar as suas condições.
Interrupção é outro caminho de saída. Se a espera lança InterruptedException, a rotina precisa de uma política: propagar para uma camada que decide, ou terminar preservando o sinal quando não pode declarar essa exceção. Ignorar a interrupção e voltar imediatamente a esperar pode impedir o encerramento solicitado. A decisão deve estar ligada à responsabilidade da rotina, sem tratar toda interrupção como sucesso.
O Future descreve um resultado, não todos os efeitos
submit devolve um Future para acompanhar a tarefa. get pode esperar por um resultado normal ou comunicar uma falha da tarefa através de ExecutionException. Uma tarefa cancelada tem outro resultado de controlo. isDone não significa necessariamente sucesso: também abrange falha e cancelamento.
Um prazo passado a get limita a espera do chamador. TimeoutException não prova que a tarefa parou, que nunca começou ou que não escreveu num sistema externo. Se o chamador pretende repetir a operação, precisa de conhecer o estado do trabalho anterior e a política de identidade ou reconciliação dos efeitos.
A mesma distinção aplica-se a cancel(true). O pedido pode levar a uma interrupção, mas a operação subjacente pode não cooperar imediatamente. Mesmo depois de observar isCancelled, é necessário confirmar o resultado de uma escrita já iniciada. Não há rollback automático de bases de dados, ficheiros ou pedidos remotos. Regista separadamente o estado do Future, a terminação da tarefa e o resultado funcional.
Encerrar e conservar capacidade para avançar
shutdown fecha a admissão a novas tarefas e permite processar as já submetidas. Não espera pela sua conclusão. awaitTermination permite observar a terminação com um prazo; o chamador tem de interpretar o resultado e responder à sua própria interrupção. shutdownNow faz uma tentativa mais forte de interromper trabalho, mas não garante que todas as tarefas terminem.
A capacidade de execução também faz parte das dependências. Num executor com um único worker, uma tarefa externa que submete outra tarefa e espera por ela ocupa o único lugar que a interna precisa de usar. A fila contém trabalho, mas esse trabalho não pode começar enquanto a tarefa externa continuar à espera. Aumentar apenas o prazo não cria outro worker.
No caso desta aula, o cálculo interno é finito e pode executar diretamente na tarefa externa, eliminando a dependência. Noutros desenhos pode ser necessário compor resultados sem bloquear esse executor ou separar responsabilidades de execução. A mudança deve corresponder às dependências reais; aumentar indiscriminadamente threads pode apenas deslocar o limite para ligações, memória ou serviços externos.
Atualizações atómicas e efeitos repetidos
compareAndSet compara o valor atual com o esperado e só o troca quando coincidem. Uma tentativa falhada obriga o chamador a reler e a decidir novamente. No contador de reservas, o sucesso da troca é o ponto que concede a unidade; uma leitura anterior positiva não a concede.
As funções usadas em updateAndGet ou getAndUpdate podem ser avaliadas novamente perante contenção. Por isso devem calcular sem efeitos externos. Colocar audit.record dentro da função pode produzir vários registos para uma atualização finalmente aceite. Proteger apenas a escrita de auditoria com synchronized não impede que a função seja chamada outra vez.
Mover a auditoria para depois da atualização evita esse tipo de repetição da função, mas não cria uma transação entre o contador e o destino da auditoria. Se a gravação falhar depois da reserva, é necessário definir como reconhecer, recuperar ou compensar esse estado. A atomicidade de uma variável é uma garantia delimitada. Não deve ser apresentada como exatamente uma vez para um processo que envolve sistemas diferentes.
Revisão guiada: a última unidade
O excerto mantém o contador privado e só permite reservas, sem reposição ou resets. O construtor rejeita valores negativos. Em tryReserve, uma leitura de zero devolve false; um valor positivo permite tentar a troca para menos uma unidade. Se outro chamador venceu entretanto, o ciclo volta à leitura.
Com uma unidade e dois chamadores, ambos podem ler um. Só um compareAndSet de um para zero terá sucesso; o outro volta a ler zero e falha a reserva. O contador mantém-se não negativo, e apenas a troca aceite devolve true. O método não publica uma reserva em sistemas externos nem promete prioridade entre concorrentes.
A revisão deve verificar se existem outros caminhos que alterem o contador ou executem efeitos antes do sucesso da troca. Essas mudanças podem invalidar as premissas. Neste cenário delimitado, a pergunta pode ser resolvida acompanhando leituras e tentativas, sem executar threads. Os exercícios pedem decisões de seleção única ou múltipla; não exigem reproduzir uma corrida na máquina do aluno.
import java.util.concurrent.atomic.AtomicInteger;
final class Reservations {
private final AtomicInteger available;
Reservations(int initial) {
if (initial < 0) throw new IllegalArgumentException("Negative stock");
available = new AtomicInteger(initial);
}
boolean tryReserve() {
for (;;) {
int current = available.get();
if (current == 0) return false;
if (available.compareAndSet(current, current - 1)) return true;
}
}
}
Com uma unidade, duas leituras podem observar um, mas só uma troca de um para zero concede a reserva. A tentativa perdedora volta a verificar o contador.
Armadilhas comuns
Confundir volatile com exclusão; usar locks diferentes para estado comum; esperar por trabalho no único worker ocupado; tratar timeout como cancelamento; escrever efeitos numa função reaplicável.
Tópicos relacionados: Métodos, contratos e fluxo de controlo · Exceções, recursos e ficheiros · Streams, lambdas e ausência · Decimais, tempo e fronteiras · Módulos, entrega e diagnóstico
Protege a decisão completa e identifica o que cada sinal confirma: visibilidade, atualização, fim da espera, terminação ou resultado funcional.
Referência: JLS 25: Threads and locks · Java SE 25; no preview features