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 dosynchronized.lockInterruptibly()— bloqueia até adquirir, mas lançaInterruptedExceptionse interrompido. Permite cancelar uma thread que está aguardando um bloqueio.tryLock()— tenta uma vez, retornatrue/falseimediatamente. 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 + Condition | Monitor 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
synchronizedpara exclusão mútua simples. É automático, não vaza e a JVM o otimiza fortemente. - Recorra ao
Lockquando precisar de: um tempo limite na aquisição, a capacidade de cancelar um aguardante viainterrupt, múltiplasConditions no mesmo bloqueio, ou distinção de leitura-escrita (ReentrantReadWriteLock). - Recorra ao
Lockquando 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
finallye 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; comLock, você pode chamarunlock()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,synchronizedpode ser um pouco mais rápido. - Mais superfície para uso incorreto.
tryLockelockInterruptiblyprecisam 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.
O que observar na execução:
- O padrão
try/finallyda Seção 1 é o que todo ponto de chamada deLockprecisa. Não há proteção sintática — se você deletar ofinally, 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 retornoufalseapós aproximadamente 100 ms porque a thread detentora ainda estava em seu sleep de 500 ms. Esse é o contrato do prazo — a chamada retornafalseapós o tempo limite não importa o quê. Comsynchronized, 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
lockInterruptiblylançouInterruptedException. Compare comlock.lock()ousynchronized— nenhum responde ainterrupt()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 —notFullpara produtores,notEmptypara consumidores. Quando o produtor adicionou um item, chamousignalemnotEmptyespecificamente; apenas um consumidor foi acordado. Comwait/notifyAllem um monitor intrínseco, toda thread aguardante é acordada e verifica novamente; o par deConditionenvia o despertar para o lado certo da fila e economiza a rodada de despertar/verificação. - O
signal()(singular) em vez designalAll()é seguro aqui porque todos osawaiters 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),signalAllainda 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.