Por que você pode precisar de um ExecutorService para 1 thread?

Você pode usar o método Executors.newSingleThreadExecutor para criar um ExecutorService com um pool que inclua um único thread. A lógica do pool é a seguinte:

  • O serviço executa apenas uma tarefa por vez.
  • Se submetermos N tarefas para execução, todas as N tarefas serão executadas uma após a outra pela única thread.
  • Se o thread for interrompido, um novo thread será criado para executar as tarefas restantes.

Vamos imaginar uma situação em que nosso programa requer a seguinte funcionalidade:

Precisamos processar as solicitações do usuário em 30 segundos, mas não mais do que uma solicitação por unidade de tempo.

Criamos uma classe de tarefa para processar uma solicitação do usuário:


class Task implements Runnable {
   private final int taskNumber;

   public Task(int taskNumber) {
       this.taskNumber = taskNumber;
   }

   @Override
   public void run() {
       try {
           Thread.sleep(1000);
       } catch (InterruptedException ignored) {
       }
       System.out.printf("Processed request #%d on thread id=%d\\n", taskNumber, Thread.currentThread().getId());
   }
}
    

A classe modela o comportamento do processamento de uma solicitação recebida e exibe seu número.

Em seguida, no método main , criamos um ExecutorService para 1 thread, que usaremos para processar sequencialmente as solicitações recebidas. Como as condições da tarefa estipulam "dentro de 30 segundos", adicionamos uma espera de 30 segundos e, em seguida, forçamos a parada do ExecutorService .


public static void main(String[] args) throws InterruptedException {
   ExecutorService executorService = Executors.newSingleThreadExecutor();

   for (int i = 0; i < 1_000; i++) {
       executorService.execute(new Task(i));
   }
   executorService.awaitTermination(30, TimeUnit.SECONDS);
   executorService.shutdownNow();
}
    

Depois de iniciar o programa, o console exibe mensagens sobre o processamento da solicitação:

Pedido processado #0 no thread id=16
Pedido processado #1 no thread id=16
Pedido processado #2 no thread id=16

Pedido processado #29 no thread id=16

Após processar as requisições por 30 segundos, o executorService chama o método shutdownNow() , que para a tarefa atual (aquela que está sendo executada) e cancela todas as tarefas pendentes. Depois disso, o programa termina com sucesso.

Mas nem sempre tudo é tão perfeito, porque nosso programa pode facilmente ter uma situação em que uma das tarefas captadas pelo único thread de nosso pool funcione incorretamente e até mesmo termine nosso thread. Podemos simular esta situação para descobrir como o executorService funciona com um único thread neste caso.

Para fazer isso, enquanto uma das tarefas está sendo executada, encerramos nossa thread usando o método inseguro e obsoleto Thread.currentThread().stop() . Estamos fazendo isso intencionalmente para simular a situação em que uma das tarefas encerra o thread.

Mudaremos o método run na classe Task :


@Override
public void run() {
   try {
       Thread.sleep(1000);
   } catch (InterruptedException ignored) {
   }

   if (taskNumber == 5) {
       Thread.currentThread().stop();
   }

   System.out.printf("Processed request #%d on thread id=%d\\n", taskNumber, Thread.currentThread().getId());
}
    

Interrompemos a tarefa #5.

Vamos ver como fica a saída com a thread interrompida no final da tarefa #5:

Solicitação processada nº 0 na thread id=16
Solicitação processada nº 1 na thread id = 16
Solicitação processada nº 2 na thread id = 16
Solicitação processada nº 3 na thread id = 16
Solicitação processada nº 4 na thread id = 16
Solicitação processada nº 6 em thread id=17
Pedido processado #7 no thread id=17

Pedido processado #29 no thread id=17

Vemos que após a thread ser interrompida no final da tarefa 5, as tarefas começam a ser executadas em uma thread cujo identificador é 17, embora tenham sido executadas anteriormente na thread cujo identificador é 16. E porque nosso pool tem um único thread, isso só pode significar uma coisa: executorService substituiu o thread interrompido por um novo e continuou a executar as tarefas.

Assim, devemos usar newSingleThreadExecutor com um pool de thread único quando queremos processar tarefas sequencialmente e apenas uma de cada vez, e queremos continuar processando tarefas da fila independentemente da conclusão da tarefa anterior (por exemplo, o caso em que um de nossas tarefas mata o thread).

ThreadFactory

Quando falamos em criar e recriar tópicos, não podemos deixar de mencionarThreadFactory.

AThreadFactoryé um objeto que cria novos encadeamentos sob demanda.

Podemos criar nossa própria fábrica de criação de threads e passar uma instância dela para o método Executors.newSingleThreadExecutor(ThreadFactory threadFactory) .


ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "MyThread");
            }
        });
                    
Sobrescrevemos o método para criar um novo thread, passando um nome de thread para o construtor.

ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r, "MyThread");
                thread.setPriority(Thread.MAX_PRIORITY);
                return thread;
            }
        });
                    
Mudamos o nome e a prioridade do thread criado.

Portanto, vemos que temos 2 métodos Executors.newSingleThreadExecutor sobrecarregados. Um sem parâmetros e um segundo com um parâmetro ThreadFactory .

Usando um ThreadFactory , você pode configurar os encadeamentos criados conforme necessário, por exemplo, definindo prioridades, usando subclasses de encadeamento, adicionando um UncaughtExceptionHandler ao encadeamento e assim por diante.