Como limitar o número de acessos concorrentes a um recurso?
Esse é o trabalho de um semáforo. O JDK vem com a classe Semaphore justamente para isso: limitar quantas threads podem usar um recurso ao mesmo tempo, de uma forma que diz claramente, no próprio código, "eu quero limitar o acesso concorrente a isso".
Question
Como limitar o número de acessos concorrentes a um recurso?
What It Is
Um semáforo mantém um número fixo de permissões (permits). Uma thread chama acquire() antes de usar o recurso — o que bloqueia se não houver permissão disponível — e release() quando termina, devolvendo a permissão para outra thread usar:
javavar semaphore = new Semaphore(10);
try {
semaphore.acquire();
scope.fork(Service::readData);
} finally {
semaphore.release();
}Essa é uma abordagem muito mais explícita e reveladora de intenção do que uma alternativa comum: criar um ExecutorService dedicado, com um pool de threads de tamanho fixo, dimensionado para o limite que você quer.
javavar executor = Executors.newFixedThreadPool(10);
executor.submit(Service::readData);Esse truque funciona, mas esconde a real intenção atrás de uma abstração não relacionada (um pool de threads), e é fácil de usar mal ou perder o controle conforme a base de código cresce.
A Stream-Based Alternative
Desde que o Java adicionou os stream gatherers (JEP 485), existe uma terceira opção para esse mesmo problema quando você está processando um stream de requisições: Gatherers.mapConcurrent(maxConcurrency, mapper).
Você passa uma concorrência máxima e uma função de mapeamento. Cada mapeamento roda de forma concorrente, mas o número de mapeamentos ativos em qualquer momento nunca ultrapassa maxConcurrency — internamente, é sustentado pelo mesmo tipo de limitação baseada em permissões que um semáforo oferece, sem que você precise gerenciar um você mesmo.
Practical Example
javavar requests = List.of(/* suas requisições */);
var results = requests.stream()
.gather(Gatherers.mapConcurrent(
10, // concorrência máxima
Service::readData))
.toList();Aqui, até 10 chamadas a Service::readData rodam concorrentemente; o resto espera a vez automaticamente conforme vagas vão abrindo.
Solution and Conclusion
Prefira Semaphore quando precisar de controle explícito e reutilizável sobre o acesso a um recurso em caminhos de código arbitrários. Prefira Gatherers.mapConcurrent quando o trabalho é naturalmente expresso como um stream de requisições sendo mapeadas para respostas — isso mantém o código mais simples e a intenção óbvia.
Um aviso importante: nunca combine esse gatherer com um stream paralelo — os dois mecanismos de concorrência entram em conflito e o resultado é uma bagunça. Fique com um stream sequencial simples; mapConcurrent já gerencia a concorrência pra você.