Visão geral do java.util.concurrent

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.