W3docs

Interface Lock do Java

A interface java.util.concurrent.locks.Lock — o que ela faz além do `synchronized` e as regras para usá-la com segurança.

synchronized é a ferramenta pequena e precisa. É rápida, automática e cobre a maioria das necessidades de exclusão mútua. Mas quando você cresce além dela — quando precisa de um tempo limite, de uma forma de cancelar ou de mais de uma variável de condição — o Java tem uma segunda API de bloqueio, mais rica: a interface java.util.concurrent.locks.Lock e suas implementações. Este capítulo apresenta a interface; os dois próximos capítulos cobrem as duas implementações (ReentrantLock, ReentrantReadWriteLock) que você realmente usará.

O que a interface oferece

public interface Lock {
  void lock();
  void lockInterruptibly() throws InterruptedException;
  boolean tryLock();
  boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
  void unlock();
  Condition newCondition();
}

Seis métodos. Cinco deles tratam de adquirir ou liberar o bloqueio; um retorna uma Condition (a resposta do Lock ao wait/notify).

As quatro formas de adquirir são o que o synchronized não oferece:

  • lock() — bloqueia até adquirir. O mais próximo do synchronized.
  • lockInterruptibly() — bloqueia até adquirir, mas lança InterruptedException se interrompido. Permite cancelar uma thread que está aguardando um bloqueio.
  • tryLock() — tenta uma vez, retorna true/false imediatamente. Não bloqueia.
  • tryLock(time, unit) — tenta até um tempo limite e então desiste. A ferramenta de prevenção de deadlock do capítulo anterior ao anterior.

synchronized tem apenas um modo de aquisição — bloquear para sempre até obtê-lo. Isso é adequado para a maioria dos casos; não é adequado quando você precisa de um prazo ou de um ponto de cancelamento.

O padrão obrigatório try/finally

synchronized libera o monitor automaticamente quando o bloco termina — tanto para conclusão normal quanto para exceção. Lock não faz isso. Se você esquecer de chamar unlock, o bloqueio ficará retido para sempre e tudo o que vier depois ficará preso.

O padrão correto, sempre:

lock.lock();
try {
  // critical section
} finally {
  lock.unlock();
}

O unlock deve estar em um finally para que seja executado mesmo que o corpo lance uma exceção. Não há try-with-resources direto para Lock (ele não é AutoCloseable), mas existem padrões de invólucro que simulam isso. O padrão padrão acima é o que quase todo o código de produção usa.

tryLock e tempo limite

As duas sobrecargas de tryLock são a forma como o Lock permite lidar com "e se não conseguirmos?":

if (lock.tryLock()) {
  try {
    doWork();
  } finally {
    lock.unlock();
  }
} else {
  // didn't get the lock — do something else, maybe retry later
}
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {       // wait up to 500ms
  try {
    doWork();
  } finally {
    lock.unlock();
  }
} else {
  throw new TimeoutException("couldn't acquire " + name);
}

Essa segunda forma é o que torna possível a recuperação de deadlocks. Com synchronized, uma thread aguardando um monitor fica presa até que o detentor libere — não há saída, exceto o encerramento da JVM. Com tryLock(timeout), você desiste após um prazo e pode tentar novamente, falhar na operação ou tomar um caminho alternativo.

lockInterruptibly — aquisição de bloqueio cancelável

synchronized não responde a Thread.interrupt() enquanto aguarda. Uma thread BLOCKED em um monitor permanece bloqueada mesmo que você a interrompa — a JVM apenas define o flag e esquece.

lock.lockInterruptibly() responde. Se outra thread chamar interrupt() em você enquanto você aguarda o bloqueio, a chamada lança InterruptedException imediatamente:

try {
  lock.lockInterruptibly();
} catch (InterruptedException e) {
  Thread.currentThread().interrupt();
  return;                                              // gave up on the work
}
try {
  doWork();
} finally {
  lock.unlock();
}

Isso é essencial em código de servidor: uma requisição chega, uma thread tenta adquirir um bloqueio, a requisição é cancelada (desconexão do cliente, tempo limite de um balanceador de carga), o supervisor chama interrupt() no trabalhador. Com synchronized, o trabalhador continua aguardando; com lockInterruptibly, ele desiste.

Condition — o wait/notify compatível com Lock

O equivalente do monitor intrínseco — wait/notify em um bloco synchronized — oferece exatamente um conjunto de espera por objeto. Um único Lock pode ter vários objetos Condition:

Lock lock = new ReentrantLock();
Condition notFull  = lock.newCondition();
Condition notEmpty = lock.newCondition();

Você detém o bloqueio, usa await() em uma condição (o que libera o bloqueio e o suspende), e outra thread chama signal() na condição (o que o move para o estado BLOCKED aguardando o bloqueio). O mapeamento para wait/notify:

Lock + ConditionMonitor intrínseco
lock.lock()entrar em synchronized (obj)
condition.await()obj.wait()
condition.signal()obj.notify()
condition.signalAll()obj.notifyAll()
lock.unlock()sair de synchronized

A vantagem sobre wait/notify: múltiplas condições por bloqueio. Um buffer limitado pode ter uma condição para "não cheio" e outra para "não vazio" — produtores chamam signal(notEmpty) após inserir um item; consumidores chamam signal(notFull) após retirar. Apenas o lado correto é acordado. A abordagem de notifyAll com monitor único precisa acordar todos e torcer para o melhor.

Veremos a reescrita do buffer limitado no capítulo sobre ReentrantLock.

Quando usar Lock, quando ficar com synchronized

Uma regra de decisão pragmática:

  • Por padrão, use synchronized para exclusão mútua simples. É automático, não vaza e a JVM o otimiza fortemente.
  • Recorra ao Lock quando precisar de: um tempo limite na aquisição, a capacidade de cancelar um aguardante via interrupt, múltiplas Conditions no mesmo bloqueio, ou distinção de leitura-escrita (ReentrantReadWriteLock).
  • Recorra ao Lock quando a contenção for intensa e você precisar de uma opção de ordenação justa (new ReentrantLock(true) é a versão justa; monitores intrínsecos são injustos). A ordenação justa troca throughput por previsibilidade.

Você não deve "atualizar" synchronized para Lock sem motivo. Os dois são equivalentes para o caso básico; o restante do capítulo é sobre quando as capacidades extras compensam.

O que você abre mão

Lock tem custos que synchronized não tem:

  • Sem liberação automática. Esqueça o finally e o bloqueio vaza. A JVM não pode salvá-lo.
  • Sem verificação de aninhamento estruturado. Com synchronized, o compilador impõe o emparelhamento de bloqueio/desbloqueio; com Lock, você pode chamar unlock() de um método ou caminho diferente e o compilador não percebe.
  • Sem otimizações nativas de tempo de execução. A JVM tem otimizações especiais para monitores intrínsecos (biased locking, lock coarsening, lock elision em alguns casos) que não se aplicam ao Lock. Para código com muito baixa contenção, synchronized pode ser um pouco mais rápido.
  • Mais superfície para uso incorreto. tryLock e lockInterruptibly precisam ser emparelhados com uma verificação; omitir a verificação produz um bug silencioso de "bloqueio não adquirido".

Use Lock pelas capacidades, não pela sintaxe.

Um exemplo prático: Lock fazendo o que synchronized não consegue

O programa abaixo usa ReentrantLock (a implementação padrão de Lock) para demonstrar as três coisas que synchronized não oferece: tryLock com tempo limite, lockInterruptibly e uma Condition personalizada.

java— editable, runs on the server

O que observar na execução:

  • O padrão try/finally da Seção 1 é o que todo ponto de chamada de Lock precisa. Não há proteção sintática — se você deletar o finally, o código compila, e o bloqueio vaza na primeira vez que o corpo lançar uma exceção. Memorize a forma: lock(), try { ... } finally { unlock(); }.
  • O tryLock(100, MS) da Seção 2 retornou false após aproximadamente 100 ms porque a thread detentora ainda estava em seu sleep de 500 ms. Esse é o contrato do prazo — a chamada retorna false após o tempo limite não importa o quê. Com synchronized, essa thread teria bloqueado até o detentor liberar, sem escotilha de saída.
  • O aguardante da Seção 3 foi interrompido enquanto aguardava o bloqueio, e lockInterruptibly lançou InterruptedException. Compare com lock.lock() ou synchronized — nenhum responde a interrupt() enquanto aguarda. Essa é a diferença entre um servidor que pode cancelar requisições com tempo esgotado e um que apenas acumula threads presas.
  • A Seção 4 usou duas Conditions em um bloqueio — notFull para produtores, notEmpty para consumidores. Quando o produtor adicionou um item, chamou signal em notEmpty especificamente; apenas um consumidor foi acordado. Com wait/notifyAll em um monitor intrínseco, toda thread aguardante é acordada e verifica novamente; o par de Condition envia o despertar para o lado certo da fila e economiza a rodada de despertar/verificação.
  • O signal() (singular) em vez de signalAll() é seguro aqui porque todos os awaiters em cada condição são intercambiáveis — qualquer produtor pode preencher o slot que acabamos de abrir. Se os aguardantes não fossem intercambiáveis (por exemplo, aguardando chaves específicas diferentes), signalAll ainda seria o padrão mais seguro.

O que vem a seguir

O próximo capítulo, Java ReentrantLock, entra em detalhes sobre a implementação padrão de Lock — sua reentrância, sua política de justiça e a API de diagnóstico getHoldCount/isHeldByCurrentThread.

Prática

Prática
Você está escrevendo código que precisa adquirir um bloqueio com um prazo — desistir após 200 ms se o bloqueio não estiver disponível e executar uma ação alternativa. Qual abordagem é a correta?
Você está escrevendo código que precisa adquirir um bloqueio com um prazo — desistir após 200 ms se o bloqueio não estiver disponível e executar uma ação alternativa. Qual abordagem é a correta?
Was this page helpful?