- Introdução ao StampedLock
No artigo anterior, discutimos os bloqueios de leitura e escrita (read-write locks) e seu uso em cenários com múltiplas leituras e poucas escritas. Mas existe uma alternativa com desempenho superior disponível desde o Java 1.8: o StampedLock.
- Vantagens do StampedLock
Enquanto os bloqueios de leitura e escrita tradicionalmente possuem dois modos (leitura e escrita), o StampedLock oferece três modos operacionais:
- Bloqueio de escrita
- Bloqueio de leitura pessimista
- Bloqueio de leitura otimista
Os dois primeiros correspondem funcionalmente aos bloqueios de escrita e leitura tradicionais. No entanto, o recurso diferencial do StampedLock é a leitura otimista, que não é um bloqueio propriamente dito, mas sim uma operação que permite que múltiplos threads leiam simultaneamente sem adquirir bloqueios.
Para garantir segurança concorrente, a leitura otimista utiliza um "carimbo" (stamp) como sinal de segurança, similar ao mecanismo de versão usado em bloqueios otimistas de banco de dados.
- Diferenças entre bloqueio de escrita/leitura pessimista e bloqueios tradicionais
Quando adquiremos bloqueios de escrita ou leitura pessimista com StampedLock, o método retorna um carimbo (stamp). Para liberar o bloqueio, devemos fornecer este carimbo correspondente:
final StampedLock sl = new StampedLock();
// Exemplo de aquisição/liberação de bloqueio de leitura pessimista
long stamp = sl.readLock();
try {
// código de negócio
} finally {
sl.unlockRead(stamp);
}
// Exemplo de aquisição/liberação de bloqueio de escrita
long stamp = sl.writeLock();
try {
// código de negócio
} finally {
sl.unlockWrite(stamp);
}
- Utilizando a leitura otimista
O método tryOptimistic() implementa a leitura otimista. Como não envolve bloqueio, as variáveis compartilhadas podem ser modificadas por outros threads durante a leitura. Portanto, após a leitura, precisamos verificar se houve operações de escrito através do método validate(stamp).
class Ponto {
private int x, y;
final StampedLock sl = new StampedLock();
// Calcula a distância da origem
double distanciaDaOrigem() {
// Leitura otimista
long stamp = sl.tryOptimisticRead();
// Lê para variáveis locais
// Os dados podem ser modificados durante a leitura
int currentX = x, currentY = y;
// Verifica se houve operações de escrita durante a leitura
if (!sl.validate(stamp)) {
// Converte para bloqueio de leitura pessimista
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
// Libera bloqueio de leitura pessimista
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
No código acima, se ocorrerem operações de esccrita durante a leitura otimista, ela é convertida para bloqueio de leitura pessimista. Isso evita loops infinitos e desperdício de CPU causados por tentativas repetidas de leitura otimista.
- Precauções ao usar StampedLock
- Nunca interrompa threads bloqueadas em readLock() ou writeLock(): Chamar interrupt() em threads bloqueadas nessas operações pode causar problemas sérios no CPU. Se necessário usar interrupção, utilize readLockInterruptibly() e writeLockInterruptibly().
- Funcionalidade limitada: StampedLock oferece menos funcionalidades comparado aos bloqueios de leitura e escrita tradicionais.
- Não suporta reentrância: StampedLock não é um bloqueio reentrante, portanto não pode ser usado de forma aninhada.
- Modelos de implementação recomendados
Modelo de leitura com StampedLock:
final StampedLock sl = new StampedLock();
// Leitura otimista
long stamp = sl.tryOptimisticRead();
// Lê para variáveis locais
......
// Verifica o carimbo
if (!sl.validate(stamp)) {
// Converte para bloqueio de leitura pessimista
stamp = sl.readLock();
try {
// Lê para variáveis locais
......
} finally {
// Libera bloqueio de leitura pessimista
sl.unlockRead(stamp);
}
}
// Usa variáveis locais para operações de negócio
......
Modelo de escrita com StampedLock:
long stamp = sl.writeLock();
try {
// Escreve variáveis compartilhadas
......
} finally {
sl.unlockWrite(stamp);
}