Il metodo newFixedThreadPool della classe Executors crea un executorService con un numero fisso di thread. A differenza del metodo newSingleThreadExecutor , specifichiamo quanti thread vogliamo nel pool. Sotto il cofano, viene chiamato il seguente codice:

new ThreadPoolExecutor(nThreads, nThreads,
                                      	0L, TimeUnit.MILLISECONDS,
                                      	new LinkedBlockingQueue());

I parametri corePoolSize (il numero di thread che saranno pronti (avviati) all'avvio del servizio esecutore ) e maximumPoolSize (il numero massimo di thread che il servizio esecutore può creare) ricevono lo stesso valore: il numero di thread passati a newFixedThreadPool(nThreads ) . E possiamo passare la nostra implementazione di ThreadFactory esattamente allo stesso modo.

Bene, vediamo perché abbiamo bisogno di un tale ExecutorService .

Ecco la logica di un ExecutorService con un numero fisso (n) di thread:

  • Un massimo di n thread sarà attivo per le attività di elaborazione.
  • Se vengono inviate più di n attività, verranno mantenute in coda fino a quando i thread non si liberano.
  • Se uno dei thread fallisce e termina, verrà creato un nuovo thread per sostituirlo.
  • Qualsiasi thread nel pool è attivo finché il pool non viene chiuso.

Ad esempio, immagina di aspettare per passare i controlli di sicurezza in aeroporto. Tutti stanno in fila fino a poco prima del controllo di sicurezza, i passeggeri sono distribuiti tra tutti i checkpoint funzionanti. Se c'è un ritardo in uno dei checkpoint, la coda verrà elaborata solo dal secondo fino a quando il primo non sarà libero. E se un checkpoint chiude completamente, verrà aperto un altro checkpoint per sostituirlo e i passeggeri continueranno a essere processati attraverso due checkpoint.

Notiamo subito che anche se le condizioni sono ideali — gli n thread promessi funzionano stabilmente, e i thread che terminano con un errore vengono sempre sostituiti (cosa che le risorse limitate rendono impossibile da ottenere in un vero aeroporto) — il sistema ha ancora diversi caratteristiche spiacevoli, perché in nessun caso ci saranno più thread, anche se la coda cresce più velocemente di quanto i thread possano elaborare le attività.

Suggerisco di ottenere una comprensione pratica di come funziona ExecutorService con un numero fisso di thread. Creiamo una classe che implementa Runnable . Gli oggetti di questa classe rappresentano i nostri compiti per ExecutorService .

public class Task implements Runnable {
    int taskNumber;

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

    @Override
    public void run() {
try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Processed user request #" + taskNumber + " on thread " + Thread.currentThread().getName());
    }
}

Nel metodo run() , blocchiamo il thread per 2 secondi, simulando un certo carico di lavoro, quindi visualizziamo il numero dell'attività corrente e il nome del thread che esegue l'attività.

ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 30; i++) {
            executorService.execute(new Task(i));
        }

        executorService.shutdown();

Per cominciare, nel metodo principale , creiamo un ExecutorService e inviamo 30 attività per l'esecuzione.

Richiesta utente elaborata n. 1 sul thread pool-1-thread-2
Richiesta utente elaborata n. 0 sul thread pool-1-thread-1
Richiesta utente elaborata n. 2 sul thread pool-1-thread-3
Richiesta utente elaborata n. Thread 1-thread-3
Richiesta utente elaborata n. 3 sul thread pool-1-thread-2
Richiesta utente elaborata n. 4 sul thread pool-1-thread-1
Richiesta utente elaborata n. 8 sul thread pool-1-thread-1
Utente elaborato richiesta n. 6 sul thread pool-1-thread-3
Richiesta utente elaborata n. 7 sul thread pool-1-thread-2
Richiesta utente elaborata n. 10 sul thread pool-1-thread-3
Richiesta utente elaborata n. 9 sul thread pool-1- thread-1 thread
Richiesta utente elaborata n. 11 sul thread pool-1-thread-2
Richiesta utente elaborata n. 12 sul thread pool-1-thread-3
Richiesta utente elaborata n. 14 sul thread pool-1-thread-2
Richiesta utente elaborata n. 13 sul thread pool-1-thread-1
Richiesta utente elaborata n. 15 sul thread pool-1-thread-3
Richiesta utente elaborata n. 16 sul thread pool- Thread 1-thread-2
Richiesta utente elaborata n. 17 sul thread pool-1-thread-1
Richiesta utente elaborata n. 18 sul thread pool-1-thread-3
Richiesta utente elaborata n. 19 sul thread pool-1-thread-2
Utente elaborato richiesta n. 20 sul thread pool-1-thread-1
Richiesta utente elaborata n. 21 sul thread pool-1-thread-3
Richiesta utente elaborata n. 22 sul thread pool-1-thread-2
Richiesta utente elaborata n. 23 sul thread pool-1- thread-1 thread
Richiesta utente elaborata n. 25 sul thread pool-1-thread-2
Richiesta utente elaborata n. 24 sul thread pool-1-thread-3
Richiesta utente elaborata n. 26 sul thread pool-1-thread-1
Richiesta utente elaborata n. 27 sul thread pool-1-thread-2
Richiesta utente elaborata n. 28 sul thread pool-1-thread-3
Richiesta utente elaborata n. 29 sul thread pool- 1 filo-1 filo

L'output della console ci mostra come le attività vengono eseguite su thread diversi una volta che sono state rilasciate dall'attività precedente.

Ora aumenteremo il numero di attività a 100 e, dopo aver inviato 100 attività, chiameremo il metodo awaitTermination(11, SECONDS) . Passiamo un numero e un'unità di tempo come argomenti. Questo metodo bloccherà il thread principale per 11 secondi. Quindi chiameremo shutdownNow() per forzare l' arresto di ExecutorService senza attendere il completamento di tutte le attività.

ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 100; i++) {
            executorService.execute(new Task(i));
        }

        executorService.awaitTermination(11, SECONDS);

        executorService.shutdownNow();
        System.out.println(executorService);

Alla fine, mostreremo le informazioni sullo stato di executorService .

Ecco l'output della console che otteniamo:

Richiesta utente elaborata n. 0 sul thread pool-1-thread-1
Richiesta utente elaborata n. 2 sul thread pool-1-thread-3
Richiesta utente elaborata n. 1 sul thread pool-1-thread-2
Richiesta utente elaborata n. 4 sul thread pool- Thread 1-thread-3
Richiesta utente elaborata n. 5 sul thread pool-1-thread-2
Richiesta utente elaborata n. 3 sul thread pool-1-thread-1
Richiesta utente elaborata n. 6 sul thread pool-1-thread-3
Utente elaborato richiesta n. 7 sul thread pool-1-thread-2
Richiesta utente elaborata n. 8 sul thread pool-1-thread-1
Richiesta utente elaborata n. 9 sul thread pool-1-thread-3
Richiesta utente elaborata n. 11 sul thread pool-1- thread-1 thread
Richiesta utente elaborata n. 10 sul thread pool-1-thread-2
Richiesta utente elaborata n. 13 sul thread pool-1-thread-1
Richiesta utente elaborata n. 14 sul thread pool-1-thread-2
Richiesta utente elaborata n. 12 sul thread pool-1-thread-3
java.util.concurrent.ThreadPoolExecutor@452b3a41[Chiusura in corso, dimensione pool = 3, thread attivi = 3 , attività in coda = 0, attività completate = 15]
Richiesta utente elaborata n. 17 sul thread pool-1-thread-3
Richiesta utente elaborata n. 15 sul thread pool-1-thread-1
Richiesta utente elaborata n. 16 sul thread pool-1 -2 filo

Questo è seguito da 3 InterruptedExceptions , generati dai metodi sleep da 3 attività attive.

Possiamo vedere che quando il programma termina, vengono eseguite 15 attività, ma il pool aveva ancora 3 thread attivi che non hanno terminato l'esecuzione delle loro attività. Il metodo interrupt() viene chiamato su questi tre thread, il che significa che l'attività verrà completata, ma nel nostro caso il metodo sleep genera un'eccezione InterruptedException . Vediamo anche che dopo la chiamata al metodo shutdownNow() , la coda delle attività viene cancellata.

Pertanto, quando si utilizza un ExecutorService con un numero fisso di thread nel pool, assicurarsi di ricordare come funziona. Questo tipo è adatto per compiti con un carico costante noto.

Ecco un'altra domanda interessante: se devi usare un esecutore per un singolo thread, quale metodo dovresti chiamare? newSingleThreadExecutor() o newFixedThreadPool(1) ?

Entrambi gli esecutori avranno un comportamento equivalente. L'unica differenza è che il metodo newSingleThreadExecutor() restituirà un esecutore che non può essere successivamente riconfigurato per utilizzare thread aggiuntivi.