Visão geral do java.util.concurrent
1. Visão geral
O pacotejava.util.concurrent fornece ferramentas para a criação de aplicativos simultâneos.
Neste artigo, faremos uma visão geral de todo o pacote.
2. Componentes principais
Ojava.util.concurrent contém muitos recursos para discutir em um único artigo. Neste artigo, focaremos principalmente alguns dos utilitários mais úteis deste pacote, como:
-
Executor
-
ExecutorService
-
ScheduledExecutorService
-
Futuro
-
CountDownLatch
-
CyclicBarrier
-
Semáforo
-
ThreadFactory
-
BlockingQueue
-
DelayQueue
-
Fechaduras
-
Phaser
Você também pode encontrar muitos artigos dedicados a aulas individuais aqui.
2.1. Executor
Executor é uma interface que representa um objeto que executa tarefas fornecidas.
Depende da implementação específica (de onde a chamada é iniciada) se a tarefa deve ser executada em um encadeamento novo ou atual. Portanto, usando essa interface, podemos dissociar o fluxo de execução da tarefa do mecanismo real de execução da tarefa.
Um ponto a ser observado aqui é queExecutor não exige estritamente que a execução da tarefa seja assíncrona. No caso mais simples, um executor pode chamar a tarefa enviada instantaneamente no segmento de chamada.
Precisamos criar um invocador para criar a instância do executor:
public class Invoker implements Executor {
@Override
public void execute(Runnable r) {
r.run();
}
}
Agora, podemos usar esse invocador para executar a tarefa.
public void execute() {
Executor executor = new Invoker();
executor.execute( () -> {
// task to be performed
});
}
O ponto a notar aqui é que se o executor não puder aceitar a tarefa para execução, ele lançaráRejectedExecutionException.
2.2. ExecutorService
ExecutorService é uma solução completa para processamento assíncrono. Ele gerencia uma fila na memória e agenda as tarefas enviadas com base na disponibilidade do encadeamento.
Para usarExecutorService,, precisamos criar uma classeRunnable.
public class Task implements Runnable {
@Override
public void run() {
// task details
}
}
Agora podemos criar a instânciaExecutorService e atribuir esta tarefa. No momento da criação, precisamos especificar o tamanho do conjunto de encadeamentos.
ExecutorService executor = Executors.newFixedThreadPool(10);
Se quisermos criar uma instânciaExecutorService de thread único, podemos usarnewSingleThreadExecutor(ThreadFactory threadFactory) para criar a instância.
Depois que o executor é criado, podemos usá-lo para enviar a tarefa.
public void execute() {
executor.submit(new Task());
}
Também podemos criar a instânciaRunnable ao enviar a tarefa.
executor.submit(() -> {
new Task();
});
Ele também vem com dois métodos de finalização de execução prontos para uso. O primeiro éshutdown(); ele espera até que todas as tarefas enviadas terminem de ser executadas. O outro método éshutdownNow(), queh termina imediatamente todas as tarefas pendentes / em execução.
Há também outro métodoawaitTermination(long timeout, TimeUnit unit) que bloqueia à força até que todas as tarefas tenham concluído a execução após um evento de desligamento disparado ou o tempo limite de execução ocorrer, ou o próprio thread de execução for interrompido,
try {
executor.awaitTermination( 20l, TimeUnit.NANOSECONDS );
} catch (InterruptedException e) {
e.printStackTrace();
}
2.3. ScheduledExecutorService
ScheduledExecutorService é uma interface semelhante aExecutorService,, mas pode executar tarefas periodicamente.
Executor and ExecutorService‘s methods are scheduled on the spot without introducing any artificial delay. Zero ou qualquer valor negativo significa que a solicitação precisa ser executada instantaneamente.
Podemos usar as interfacesRunnableeCallable para definir a tarefa.
public void execute() {
ScheduledExecutorService executorService
= Executors.newSingleThreadScheduledExecutor();
Future future = executorService.schedule(() -> {
// ...
return "Hello world";
}, 1, TimeUnit.SECONDS);
ScheduledFuture> scheduledFuture = executorService.schedule(() -> {
// ...
}, 1, TimeUnit.SECONDS);
executorService.shutdown();
}
ScheduledExecutorService também pode agendar a tarefaafter some given fixed delay:
executorService.scheduleAtFixedRate(() -> {
// ...
}, 1, 10, TimeUnit.SECONDS);
executorService.scheduleWithFixedDelay(() -> {
// ...
}, 1, 10, TimeUnit.SECONDS);
Aqui, o métodoscheduleAtFixedRate( Runnable command, long initialDelay, long period, TimeUnit unit ) cria e executa uma ação periódica que é invocada primeiro após o atraso inicial fornecido e, subsequentemente, com o período determinado até o encerramento da instância de serviço.
O métodoscheduleWithFixedDelay( Runnable command, long initialDelay, long delay, TimeUnit unit ) cria e executa uma ação periódica que é invocada primeiro após o atraso inicial fornecido, e repetidamente com o atraso dado entre o término da ação em execução e a invocação da próxima.
2.4. Future
Future is used to represent the result of an asynchronous operation. Inclui métodos para verificar se a operação assíncrona foi concluída ou não, obtendo o resultado calculado, etc.
Além do mais, a APIcancel(boolean mayInterruptIfRunning) cancela a operação e libera o thread em execução. Se o valor demayInterruptIfRunning for verdadeiro, o encadeamento que executa a tarefa será encerrado instantaneamente.
Caso contrário, as tarefas em andamento poderão ser concluídas.
Podemos usar o snippet de código abaixo para criar uma instância futura:
public void invoke() {
ExecutorService executorService = Executors.newFixedThreadPool(10);
Future future = executorService.submit(() -> {
// ...
Thread.sleep(10000l);
return "Hello world";
});
}
Podemos usar o seguinte snippet de código para verificar se o resultado futuro está pronto e buscar os dados se o cálculo for feito:
if (future.isDone() && !future.isCancelled()) {
try {
str = future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
Também podemos especificar um tempo limite para uma determinada operação. Se a tarefa demorar mais do que esse tempo, umTimeoutException é lançado:
try {
future.get(10, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
e.printStackTrace();
}
2.5. CountDownLatch
CountDownLatch (introduzido emJDK 5) é uma classe de utilitário que bloqueia um conjunto de threads até que alguma operação seja concluída.
UmCountDownLatch é inicializado com um tipocounter(Integer); este contador diminui à medida que os threads dependentes completam a execução. Mas quando o contador chega a zero, outros threads são liberados.
Você pode aprender mais sobreCountDownLatchhere.
2.6. CyclicBarrier
CyclicBarrier funciona quase da mesma forma queCountDownLatch, exceto que podemos reutilizá-lo. Ao contrário deCountDownLatch, ele permite que vários threads esperem uns pelos outros usando o métodoawait() (conhecido como condição de barreira) antes de chamar a tarefa final.
Precisamos criar uma instância de tarefaRunnable para iniciar a condição de barreira:
public class Task implements Runnable {
private CyclicBarrier barrier;
public Task(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public void run() {
try {
LOG.info(Thread.currentThread().getName() +
" is waiting");
barrier.await();
LOG.info(Thread.currentThread().getName() +
" is released");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
Agora podemos chamar alguns threads para competir pela condição de barreira:
public void start() {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
// ...
LOG.info("All previous tasks are completed");
});
Thread t1 = new Thread(new Task(cyclicBarrier), "T1");
Thread t2 = new Thread(new Task(cyclicBarrier), "T2");
Thread t3 = new Thread(new Task(cyclicBarrier), "T3");
if (!cyclicBarrier.isBroken()) {
t1.start();
t2.start();
t3.start();
}
}
Aqui, o métodoisBroken() verifica se alguma das threads foi interrompida durante o tempo de execução. Sempre devemos executar essa verificação antes de executar o processo real.
2.7. Semaphore
OSemaphore é usado para bloquear o acesso de nível de thread a alguma parte do recurso físico ou lógico. Um semáforo contém um conjunto de permissões; sempre que um encadeamento tenta entrar na seção crítica, ele precisa verificar o semáforo se uma permissão está disponível ou não.
Se uma licença não estiver disponível (viatryAcquire()), o thread não tem permissão para pular para a seção crítica; entretanto, se a licença estiver disponível, o acesso é concedido e o contador de licenças diminui.
Assim que o thread em execução libera a seção crítica, novamente o contador de licenças aumenta (feito pelo métodorelease()).
Podemos especificar um tempo limite para adquirir acesso usando o métodotryAcquire(long timeout, TimeUnit unit).
Também podemos verificar o número de licenças disponíveis ou o número de threads aguardando para adquirir o semáforo.
O seguinte snippet de código pode ser usado para implementar um semáforo:
static Semaphore semaphore = new Semaphore(10);
public void execute() throws InterruptedException {
LOG.info("Available permit : " + semaphore.availablePermits());
LOG.info("Number of threads waiting to acquire: " +
semaphore.getQueueLength());
if (semaphore.tryAcquire()) {
try {
// ...
}
finally {
semaphore.release();
}
}
}
Podemos implementar uma estrutura de dados comoMutex usandoSemaphore. Mais detalhes nestecan be found here.
2.8. ThreadFactory
Como o nome sugere,ThreadFactory atua como um pool de threads (não existente) que cria uma nova thread sob demanda. Elimina a necessidade de muita codificação padrão para implementar mecanismos eficientes de criação de encadeamentos.
Podemos definir umThreadFactory:
public class exampleThreadFactory implements ThreadFactory {
private int threadId;
private String name;
public exampleThreadFactory(String name) {
threadId = 1;
this.name = name;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, name + "-Thread_" + threadId);
LOG.info("created new thread with id : " + threadId +
" and name : " + t.getName());
threadId++;
return t;
}
}
Podemos usar este métodonewThread(Runnable r) para criar um novo thread em tempo de execução:
exampleThreadFactory factory = new exampleThreadFactory(
"exampleThreadFactory");
for (int i = 0; i < 10; i++) {
Thread t = factory.newThread(new Task());
t.start();
}
2.9. BlockingQueue
Na programação assíncrona, um dos padrões de integração mais comuns é oproducer-consumer pattern. O pacotejava.util.concurrent vem com uma estrutura de dados conhecida comoBlockingQueue - que pode ser muito útil nesses cenários assíncronos.
Mais informações e um exemplo prático sobre isso estão disponíveishere.
2.10. DelayQueue
DelayQueue é uma fila de bloqueio de tamanho infinito de elementos onde um elemento só pode ser puxado se o seu tempo de expiração (conhecido como atraso definido pelo usuário) for concluído. Portanto, o elemento superior (head) terá a maior quantidade de atraso e será pesquisado por último.
Mais informações e um exemplo prático sobre isso estão disponíveishere.
2.11. Locks
Não surpreendentemente,Lock é um utilitário para bloquear o acesso de outros threads a um determinado segmento de código, além do thread que o está executando atualmente.
A principal diferença entre um bloqueio e um bloco sincronizado é que o bloco sincronizado está totalmente contido em um método; no entanto, podemos ter a operação de bloqueio () e desbloqueio () da API de bloqueio em métodos separados.
Mais informações e um exemplo prático sobre isso estão disponíveishere.
2.12. Phaser
Phaser é uma solução mais flexível do queCyclicBarriereCountDownLatch - usado para atuar como uma barreira reutilizável na qual o número dinâmico de threads precisa esperar antes de continuar a execução. Podemos coordenar várias fases de execução, reutilizando uma instânciaPhaser para cada fase do programa.
Mais informações e um exemplo prático sobre isso estão disponíveishere.
3. Conclusão
Neste artigo de visão geral de alto nível, nos concentramos nos diferentes utilitários disponíveis do pacotejava.util.concurrent.
Como sempre, o código-fonte completo está disponívelover on GitHub.