A implementação de caches locais em Java é crucial para otimizar o desempenho de aplicações, reduzindo a latência e a carga sobre recursos externos. Este artigo explora duas abordagens eficaezs para gerenciar caches de forma thread-safe e eficiente: a utilização de FutureTask para computações concorrentes e a aplicação de objetos imutáveis com a palavra-chave volatile para garantir a visibilidade e a segurança.
Gerenciamento de Computações Concorrentes com FutureTask
Ao implementar um cache para resultados de operações computacionalmente intensivas, um desafio comum é evitar que a mesma computação seja executada múltiplas vezes por threads concorrentes. Uma solução robusta envolve armazenar instâncias de Future em vez dos valores finais no cache, utilizando um ConcurrentHashMap. Isso garante que, se uma computação já estiver em andamento, outras threads aguardem seu resultado em vez de iniciar uma nova execução.
A classe FutureTask, que implementa Future e Runnable, é ideal para este cenário. Ela encapsula uma computação (definida por um Callable) e permite que seu resultado seja obtido após a conclusão. No entanto, é vital tratar casos de cancelamento e exceções, removendo a entrada do cache para evitar "poluição do cache" com resultados inválidos ou incompletos.
import java.util.concurrent.*;
// Interface genérica para uma função computável
interface Computable<K, R> {
R compute(K entrada) throws InterruptedException;
}
/**
* Implementação de um serviço computável com cache de memoização,
* utilizando FutureTask para gerenciar computações concorrentes.
*
* @param <K> Tipo da chave de entrada
* @param <R> Tipo do resultado da computação
*/
public class ServicoComputacaoCacheado<K, R> implements Computable<K, R> {
private final Map<K, Future<R>> cacheDeResultados = new ConcurrentHashMap<>();
private final Computable<K, R> funcaoComputavel;
public ServicoComputacaoCacheado(Computable<K, R> funcaoComputavel) {
this.funcaoComputavel = funcaoComputavel;
}
@Override
public R compute(K entrada) throws InterruptedException {
while (true) { // Loop para tentar obter ou iniciar a computação
Future<R> resultadoFuturo = cacheDeResultados.get(entrada);
if (resultadoFuturo == null) {
Callable<R> tarefaDeCalculo = () -> funcaoComputavel.compute(entrada);
FutureTask<R> novaTarefa = new FutureTask<>(tarefaDeCalculo);
resultadoFuturo = cacheDeResultados.putIfAbsent(entrada, novaTarefa);
if (resultadoFuturo == null) { // A nova tarefa foi adicionada com sucesso
resultadoFuturo = novaTarefa;
novaTarefa.run(); // Inicia a computação no thread atual
}
}
try {
return resultadoFuturo.get(); // Aguarda e retorna o resultado
} catch (CancellationException ex) {
cacheDeResultados.remove(entrada, resultadoFuturo); // Remove se a tarefa foi cancelada
// Tenta novamente a computação na próxima iteração do loop
} catch (ExecutionException e) {
// Desembrulha e lança a exceção original da computação
throw DesembrulhadorExcecao.desembrulhar(e.getCause());
}
}
}
}
/**
* Utilitário para desembrulhar e relançar exceções.
*/
class DesembrulhadorExcecao {
public static RuntimeException desembrulhar(Throwable causa) {
if (causa instanceof RuntimeException) {
return (RuntimeException) causa;
} else if (causa instanceof Error) {
throw (Error) causa;
} else {
throw new IllegalStateException("Exceção verificada inesperada", causa);
}
}
}
Cache com Objetos Imutáveis e Visibilidade Volatile
Outra estratégia eficaz para garantir a segurança de threads em caches é utilizar objetos imutáveis. Um objeto imutável, uma vez criado, não pode ter seu estado alterado. Isso simplifica drasticamente a lógica de concorrência, pois não há necessidade de bloqueios para acesso aos dados do objeto em si.
Para um cache que armazena um único valor e seus resultados assoicados, criar uma classe imutável para o item de cache é uma prática recomendada. Quando o valor do cache precisa ser atualizado, uma nova instância imutável do cache é criada e publicada. Para garantir que todas as threads vejam a versão mais recente da instância do cache, a referência ao objeto de cache deve ser declarada como volatile.
É importante ressaltar que, se o objeto imutável contiver referências a outros objetos mutáveis (como arrays), cópias defensivas devem ser feitas no construtor e ao retornar esses objetos para o exterior, a fim de preservar a imutabilidade do cache.
import java.math.BigDecimal;
import java.util.Arrays;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
/**
* Representa uma entrada de cache imutável para um número e seus fatores.
*/
public final class ArmazenamentoDeFatoresImutavel {
private final BigDecimal valorNumerico;
private final BigDecimal[] fatoresCalculados;
public ArmazenamentoDeFatoresImutavel(BigDecimal valor, BigDecimal[] fatores) {
this.valorNumerico = valor;
// Cópia defensiva para garantir a imutabilidade do array
this.fatoresCalculados = Arrays.copyOf(fatores, fatores.length);
}
/**
* Consulta os fatores para um dado número.
* @param entrada O número para o qual consultar os fatores.
* @return Os fatores do número se estiverem em cache e corresponderem, ou null.
*/
public BigDecimal[] consultarFatores(BigDecimal entrada) {
if (valorNumerico == null || !valorNumerico.equals(entrada)) {
return null; // Não há correspondência no cache
} else {
// Retorna uma cópia defensiva para evitar modificações externas
return Arrays.copyOf(fatoresCalculados, fatoresCalculados.length);
}
}
}
/**
* Um Servlet de exemplo que utiliza um cache baseado em objetos imutáveis e volatile.
*/
public class ServidorDeFatoracaoComCache implements Servlet {
// A referência ao objeto de cache é volatile para garantir a visibilidade
// das atualizações entre as threads.
private volatile ArmazenamentoDeFatoresImutavel cacheFatores =
new ArmazenamentoDeFatoresImutavel(null, null);
@Override
public void service(ServletRequest requisicao, ServletResponse resposta) throws ServletException, IOException {
BigDecimal numeroEntrada = extrairNumeroDaRequisicao(requisicao); // Método auxiliar omitido
BigDecimal[] resultadosFatores = cacheFatores.consultarFatores(numeroEntrada);
if (resultadosFatores == null) {
resultadosFatores = calcularFatoresPara(numeroEntrada); // Método auxiliar omitido
// Cria e publica uma nova instância imutável do cache.
// A palavra-chave 'volatile' garante que outras threads verão essa nova instância.
cacheFatores = new ArmazenamentoDeFatoresImutavel(numeroEntrada, resultadosFatores);
}
// Lógica para enviar a resposta (omitida para brevidade)
// enviarResposta(resposta, resultadosFatores);
}
// --- Métodos auxiliares omitidos para brevidade ---
private BigDecimal extrairNumeroDaRequisicao(ServletRequest requisicao) {
// Implementação para extrair o número da requisição
return BigDecimal.valueOf(Math.random() * 100); // Exemplo simulado
}
private BigDecimal[] calcularFatoresPara(BigDecimal numero) {
// Implementação para calcular os fatores de um número
return new BigDecimal[]{BigDecimal.ONE, numero}; // Exemplo simulado
}
// ---------------------------------------------------
}