Lock é um mecanismo de sincronização que fornece acesso exclusivo a seções críticas de código em aplicações multithread. De acordo com a Oracle, 2024, a interface Lock oferece um controle de sincronização mais flexível em comparação com os blocos synchronized tradicionais, incluindo tentativas de aquisição com tempo limite e suporte a múltiplas filas de espera.
Principais Pontos
Lock é uma interface do pacote java.util.concurrent.locks que fornece operações explícitas de bloqueio e desbloqueio para sincronizar o acesso a dados. Ao contrário de synchronized, Lock dá ao desenvolvedor controle total sobre o mecanismo de bloqueio.
A interface Lock foi introduzida no Java 5 como uma alternativa ao mecanismo synchronized integrado. Os principais métodos são lock, unlock, tryLock e lockInterruptibly. Os bloqueios ajudam a organizar o acesso seguro a dados em um ambiente multithread, evitando condições de corrida e corrupção de dados.
A principal vantagem do Lock sobre o synchronized é a flexibilidade. O desenvolvedor pode tentar adquirir um bloqueio com tempo limite, verificar sua disponibilidade sem bloquear ou organizar múltiplas filas de espera com diferentes prioridades.
Antes da interface Lock aparecer no Java 5, o único método de sincronização era o synchronized, que sofria de limitações: sem timeouts, sem espera interrompível e uma única fila. Doug Lea projetou o pacote java.util.concurrent, incluindo o Lock como um bloco fundamental.
Um bloqueio gerencia o acesso através de uma bandeira de estado interna e uma fila de espera. Quando uma thread chama lock(), o mecanismo verifica se o bloqueio está livre e o adquire ou coloca a thread na fila até que seja liberado.
No centro de qualquer bloqueio está uma operação atômica de comparação e troca (CAS). Ao chamar lock(), a thread tenta definir atomicamente a bandeira de ocupado. Se a bandeira já estiver definida, a thread bloqueia. Ao chamar unlock(), a bandeira é limpa e uma thread em espera é despertada.
import java.util.concurrent.locks.ReentrantLock
val lock = ReentrantLock()
fun performTask() {
lock.lock()
try {
// seção crítica
println("A thread ${Thread.currentThread().name} está trabalhando")
} finally {
lock.unlock()
}
}
ReentrantLock usa internamente uma lista duplamente ligada (fila de bloqueio CLH) onde cada thread em espera é representada por um nó. Quando o bloqueio é liberado, o nó principal da fila é despertado. O modo justo (fair) garante ordenação FIFO, enquanto o modo injusto permite que uma nova thread adquira o bloqueio antes das que esperam para melhorar a vazão.
No ecossistema Java moderno, existem várias implementações de bloqueios, cada uma otimizada para cenários específicos. Escolher o bloqueio certo impacta diretamente o desempenho e a confiabilidade de uma aplicação multithread.
ReentrantLock é a implementação básica e mais usada do Lock. Ela suporta reentrância pela mesma thread: se uma thread já possui o bloqueio, chamar lock() novamente não a bloqueia. Isso evita deadlocks em chamadas recursivas.
ReadWriteLock separa os bloqueios em dois modos: leitura e escrita. Várias threads podem manter o bloqueio de leitura simultaneamente, mas a escrita requer acesso exclusivo. Isso melhora significativamente o desempenho sob leituras frequentes e escritas raras.
StampedLock é a implementação mais nova, introduzida no Java 8. Ela suporta três modos: escrita, leitura e leitura otimista. A leitura otimista não bloqueia outras threads e valida os dados após a leitura, proporcionando um ganho de desempenho de 10-20% sobre o ReadWriteLock.
| Bloqueio | Versão Java | Modos | Desempenho |
|---|---|---|---|
| ReentrantLock | Java 5 | exclusivo | alto |
| ReadWriteLock | Java 5 | leitura + escrita | médio |
| StampedLock | Java 8 | leitura + escrita + otimista | muito alto |
ReentrantLock é a implementação de Lock mais popular, oferecendo vários recursos indisponíveis no synchronized. Compreender suas características é essencial para o trabalho eficaz com multithreading.
O construtor do ReentrantLock aceita um parâmetro fair. Quando true, o bloqueio garante ordem FIFO; quando false, uma nova thread pode adquirir o bloqueio antes das que esperam. O modo justo evita inanição, mas reduz a vazão em 10-20% devido à sobrecarga de manter a fila.
Ao contrário de synchronized, ReentrantLock suporta tryLock com tempo limite. Se o bloqueio não puder ser adquirido dentro do tempo especificado, a thread continua a execução em vez de bloquear indefinidamente. O método lockInterruptibly permite interromper uma thread em espera através de Thread.interrupt().
val lock = ReentrantLock()
fun tryTask() {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
println("Bloqueio adquirido")
} finally {
lock.unlock()
}
} else {
println("Falha ao adquirir o bloqueio")
}
}
ReentrantLock suporta múltiplas variáveis de condição através do método newCondition(). Cada Condition tem sua própria fila de espera, permitindo cenários de ativação complexos. Os métodos await() e signal() substituíram wait() e notify() dos blocos synchronized, mas com suporte a múltiplas filas.
ReadWriteLock e StampedLock abordam a otimização do acesso quando as leituras predominam sobre as escritas. Eles são significativamente mais eficientes que o ReentrantLock em cenários onde a leitura ocorre com mais frequência que a escrita.
A interface ReadWriteLock contém dois métodos: readLock() e writeLock(). O bloqueio de leitura pode ser mantido por várias threads simultaneamente, enquanto o bloqueio de escrita é exclusivo. Um exemplo típico é um cache thread-safe: muitas threads leem dados enquanto apenas uma os atualiza periodicamente.
class SafeCache<K, V> {
private val map = mutableMapOf<K, V>()
private val rwLock = ReentrantReadWriteLock()
fun get(key: K): V? {
rwLock.readLock().lock()
return try { map[key] } finally { rwLock.readLock().unlock() }
}
fun put(key: K, value: V) {
rwLock.writeLock().lock()
return try { map[key] = value } finally { rwLock.writeLock().unlock() }
}
}
StampedLock adiciona um terceiro modo — tryOptimisticRead. Este modo não bloqueia outras threads, mas apenas registra um carimbo (stamp) de estado. Após a leitura, o desenvolvedor chama validate(stamp) para verificar se os dados mudaram durante a leitura. Se os dados mudaram, a operação deve ser repetida.
Em aplicações móveis, os bloqueios são usados para coordenar o acesso a dados compartilhados entre threads. No entanto, seu uso requer cautela especial devido aos recursos limitados do dispositivo e à necessidade de manter a capacidade de resposta da interface.
No Android, ReentrantLock é útil ao trabalhar com Room, caches e arquivos. É importante lembrar: nunca adquira um bloqueio na thread principal. Para código assíncrono, são preferíveis corrotinas e Mutex do kotlinx.coroutines, que suspendem a corrotina em vez de bloquear a thread.
No iOS, o NSLock padrão é usado com menos frequência — os desenvolvedores preferem DispatchQueue com flags barrier ou os_unfair_lock. O Swift 5.7+ fornece mecanismos modernos de sincronização através de atores, que protegem o estado automaticamente.
import Foundation
actor DataStore {
private var items: [String] = []
func add(_ item: String) {
items.append(item)
}
func getAll() -> [String] {
items
}
}
Para evitar deadlocks, siga uma ordem consistente de aquisição em todo o projeto. Use tryLock com tempo limite em vez de lock() sempre que o bloqueio prolongado for possível. Considere usar algoritmos Lock-Free (AtomicReference, ConcurrentHashMap) em vez de bloqueios tradicionais.
O uso de Lock requer disciplina e adesão a várias regras que previnem deadlocks e degradação de desempenho. Essas práticas foram desenvolvidas pela comunidade Java ao longo de 20 anos de uso do pacote java.util.concurrent.
O padrão mais importante é lock no finally. Independentemente de a seção crítica ser concluída com sucesso ou lançar uma exceção, o bloqueio deve ser liberado. Isso garante que outras threads não fiquem bloqueadas para sempre devido a um único erro. Em Kotlin, esse padrão é elegantemente resolvido através da extensão withLock.
A seção crítica deve ser o mais curta possível. Nunca realize E/S, solicitações de rede ou cálculos prolongados dentro de um bloqueio. Se precisar ler dados de um servidor, obtenha-os primeiro e adquira o bloqueio apenas para atualizar o estado compartilhado. Isso reduz a contenção e melhora a vazão do sistema.
Para prevenir deadlocks ao trabalhar com múltiplos bloqueios, estabeleça uma ordem global de aquisição em todo o projeto. Se lockA for adquirido primeiro, depois lockB — qualquer sequência inversa deve ser proibida pelas regras de revisão de código. Use analisadores estáticos como SpotBugs e IntelliJ Inspections para verificação automática.
Perguntas Frequentes
Lock é uma interface explícita com suporte a tempo limite e espera interrompível. synchronized adquire e libera automaticamente o monitor, mas não permite usar tryLock, lockInterruptibly ou múltiplas Conditions. Lock é mais flexível, mas requer liberação manual no finally.
Um bloqueio justo garante ordem FIFO: a thread que esperou mais tempo recebe o bloqueio primeiro. Um bloqueio injusto pode conceder acesso a uma nova thread antes das que esperam, o que aumenta a vazão, mas pode causar inanição das threads em espera.
Siga uma ordem fixa para adquirir todos os bloqueios, use tryLock com tempo limite em vez de lock() incondicional e minimize o número de bloqueios mantidos simultaneamente. O uso de estruturas de dados Lock-Free também reduz o risco de deadlock.
Condition é o análogo de wait/notify para Lock, permitindo múltiplas filas de espera independentes. Cada chamada newCondition() cria uma fila separada, fornecendo um controle mais preciso sobre a ativação de threads em comparação com a fila única do synchronized.
Para Android com corrotinas, use Mutex do kotlinx.coroutines — ele suspende a corrotina em vez de bloquear a thread. Para iOS com Swift 5.7+, os atores são preferíveis, pois sincronizam automaticamente o acesso ao estado. Reserve ReentrantLock para código legado e cenários de baixo nível.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também