Perché potresti aver bisogno di un ExecutorService per 1 thread?

È possibile usare il metodo Executors.newSingleThreadExecutor per creare un ExecutorService con un pool che include un singolo thread. La logica del pool è la seguente:

  • Il servizio esegue solo un'attività alla volta.
  • Se inviamo N attività per l'esecuzione, tutte le N attività verranno eseguite una dopo l'altra dal singolo thread.
  • Se il thread viene interrotto, verrà creato un nuovo thread per eseguire tutte le attività rimanenti.

Immaginiamo una situazione in cui il nostro programma richiede la seguente funzionalità:

Dobbiamo elaborare le richieste degli utenti entro 30 secondi, ma non più di una richiesta per unità di tempo.

Creiamo una classe di attività per l'elaborazione di una richiesta utente:


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());
   }
}
    

La classe modella il comportamento dell'elaborazione di una richiesta in arrivo e ne visualizza il numero.

Successivamente, nel metodo principale , creiamo un ExecutorService per 1 thread, che utilizzeremo per elaborare in sequenza le richieste in arrivo. Poiché le condizioni dell'attività stabiliscono "entro 30 secondi", aggiungiamo 30 secondi di attesa e quindi interrompiamo forzatamente 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();
}
    

Dopo aver avviato il programma, la console visualizza i messaggi sull'elaborazione della richiesta:

Richiesta elaborata n. 0 su thread id=16
Richiesta elaborata n. 1 su thread id=16
Richiesta elaborata n. 2 su thread id=16

Richiesta elaborata n. 29 su thread id=16

Dopo aver elaborato le richieste per 30 secondi, executorService chiama il metodo shutdownNow() , che interrompe l'attività corrente (quella in esecuzione) e annulla tutte le attività in sospeso. Successivamente, il programma termina correttamente.

Ma non sempre tutto è così perfetto, perché il nostro programma potrebbe facilmente avere una situazione in cui uno dei compiti raccolti dall'unico thread del nostro pool funziona in modo errato e termina persino il nostro thread. Possiamo simulare questa situazione per capire come funziona executorService con un singolo thread in questo caso.

Per fare ciò, mentre una delle attività viene eseguita, terminiamo il nostro thread utilizzando il metodo Thread.currentThread().stop() non sicuro e obsoleto. Lo stiamo facendo intenzionalmente per simulare la situazione in cui una delle attività termina il thread.

Cambieremo il metodo run nella 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());
}
    

Interrompiamo l'attività n. 5.

Vediamo come appare l'output con il thread interrotto alla fine dell'attività n. 5:

Richiesta elaborata #0 su thread id=16
Richiesta elaborata #1 su thread id=16
Richiesta elaborata #2 su thread id=16
Richiesta elaborata #3 su thread id=16
Richiesta elaborata #4 su thread id=16
Richiesta elaborata #6 su thread id=17
Richiesta elaborata n. 7 su thread id=17

Richiesta elaborata n. 29 su thread id=17

Vediamo che dopo che il thread viene interrotto alla fine del task 5, i task iniziano ad essere eseguiti in un thread il cui identificatore è 17, sebbene fossero stati precedentemente eseguiti sul thread il cui identificatore è 16. E poiché il nostro pool ha un thread singolo, questo può significare solo una cosa: executorService ha sostituito il thread interrotto con uno nuovo e ha continuato a eseguire le attività.

Pertanto, dovremmo usare newSingleThreadExecutor con un pool a thread singolo quando vogliamo elaborare le attività in sequenza e solo una alla volta e vogliamo continuare a elaborare le attività dalla coda indipendentemente dal completamento dell'attività precedente (ad esempio il caso in cui uno dei nostri compiti uccide il filo).

ThreadFactory

Quando si parla di creare e ricreare thread, non possiamo fare a meno di menzionareThreadFactory.

UNThreadFactoryè un oggetto che crea nuovi thread su richiesta.

Possiamo creare la nostra fabbrica di creazione di thread e passarne un'istanza al metodo Executors.newSingleThreadExecutor(ThreadFactory threadFactory) .


ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "MyThread");
            }
        });
                    
Sovrascriviamo il metodo per la creazione di un nuovo thread, passando il nome di un thread al costruttore.

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;
            }
        });
                    
Abbiamo cambiato il nome e la priorità del thread creato.

Quindi vediamo che abbiamo 2 metodi Executors.newSingleThreadExecutor sovraccarichi . Uno senza parametri e un secondo con un parametro ThreadFactory .

Usando un ThreadFactory , puoi configurare i thread creati secondo necessità, ad esempio impostando le priorità, usando le sottoclassi dei thread, aggiungendo un UncaughtExceptionHandler al thread e così via.