Metoda newFixedThreadPool a clasei Executors creează un executorService cu un număr fix de fire. Spre deosebire de metoda newSingleThreadExecutor , specificăm câte fire dorim în pool. Sub capotă, se numește următorul cod:

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

Parametrii corePoolSize (numărul de fire care vor fi gata (pornite) când pornește serviciul executor ) și maximumPoolSize (numărul maxim de fire de execuție pe care serviciul executor le poate crea) primesc aceeași valoare — numărul de fire transmise către newFixedThreadPool(nThreads) ) . Și putem trece propria noastră implementare a ThreadFactory exact în același mod.

Ei bine, să vedem de ce avem nevoie de un astfel de ExecutorService .

Iată logica unui ExecutorService cu un număr fix (n) de fire:

  • Maximum n fire vor fi active pentru procesarea sarcinilor.
  • Dacă sunt trimise mai mult de n sarcini, acestea vor fi reținute în coadă până când firele de execuție devin libere.
  • Dacă unul dintre fire de execuție eșuează și se termină, un nou thread va fi creat pentru a-i lua locul.
  • Orice thread din pool este activ până când pool-ul este oprit.

De exemplu, imaginați-vă că așteptați să treceți prin securitate la aeroport. Toți stau într-o singură linie până când imediat înainte de controlul de securitate, pasagerii sunt repartizați între toate punctele de control de lucru. Dacă există o întârziere la unul dintre punctele de control, coada va fi procesată doar de cel de-al doilea până când primul este liber. Și dacă un punct de control se închide complet, atunci un alt punct de control va fi deschis pentru a-l înlocui, iar pasagerii vor continua să fie procesați prin două puncte de control.

Vom observa imediat că, chiar dacă condițiile sunt ideale - n firele promise funcționează stabil, iar firele care se termină cu o eroare sunt întotdeauna înlocuite (ceva pe care resursele limitate fac imposibil de realizat într-un aeroport real) - sistemul mai are mai multe caracteristici neplăcute, deoarece sub nicio formă nu vor exista mai multe fire de execuție, chiar dacă coada crește mai repede decât pot procesa firele de execuție sarcini.

Vă sugerez să obțineți o înțelegere practică a modului în care funcționează ExecutorService cu un număr fix de fire. Să creăm o clasă care implementează Runnable . Obiectele acestei clase reprezintă sarcinile noastre pentru 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());
    }
}

În metoda run() , blocăm firul de execuție timp de 2 secunde, simulând o sarcină de lucru, apoi afișăm numărul sarcinii curente și numele firului de execuție care execută sarcina.

ExecutorService executorService = Executors.newFixedThreadPool(3);

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

        executorService.shutdown();

Pentru început, în metoda principală , creăm un ExecutorService și trimitem 30 de sarcini pentru execuție.

Solicitarea utilizatorului #1 procesată pe firul pool-1-thread-2
Cererea utilizatorului procesată #0 pe firul pool-1-thread-1
Cererea utilizatorului #2 procesată pe firul pool-1-thread-3
Cererea utilizatorului procesată #5 pe pool- Fir 1-thread-3
Cererea utilizatorului procesată #3 pe firul pool-1-thread-2
Cererea utilizatorului procesată #4 pe firul pool-1-thread-1
Cererea utilizatorului procesată #8 pe firul pool-1-thread-1
Utilizator procesat cererea #6 pe firul pool-1-thread-3
Cererea utilizatorului procesată #7 pe firul pool-1-thread-2
Cererea utilizatorului procesată #10 pe firul pool-1-thread-3
Cererea utilizatorului procesată #9 pe pool-1- thread-1 fir
Cererea utilizatorului procesată #11 pe firul pool-1-thread-2
Solicitarea utilizatorului procesată #12 pe firul pool-1-thread-3
Solicitarea utilizatorului #14 procesată pe firul pool-1-thread-2
Cererea utilizatorului procesată #13 pe firul pool-1-thread-1
Cererea utilizatorului procesată #15 pe firul pool-1-thread-3 Cererea
utilizatorului procesată #16 pe pool- Fir 1-thread-2
Cererea utilizatorului procesată #17 pe firul pool-1-thread-1
Cererea utilizatorului procesată #18 pe firul pool-1-thread-3
Cererea utilizatorului procesată #19 pe firul pool-1-thread-2
Utilizator procesat cererea #20 pe firul pool-1-thread-1
Cererea utilizatorului procesată #21 pe firul pool-1-thread-3
Cererea utilizatorului procesată #22 pe firul pool-1-thread-2
Cererea utilizatorului procesată #23 pe pool-1- thread-1 fir
Cererea utilizatorului procesată #25 pe firul pool-1-thread-2
Solicitarea utilizatorului procesată #24 pe firul pool-1-thread-3
Cererea utilizatorului procesată #26 pe firul pool-1-thread-1
Cererea utilizatorului procesată #27 pe firul pool-1-thread-2
Cererea utilizatorului procesată #28 pe firul pool-1-thread-3 Cererea
utilizatorului procesată #29 pe pool- 1-fir-1 fir

Ieșirea consolei ne arată modul în care sarcinile sunt executate pe diferite fire odată ce sunt eliberate de sarcina anterioară.

Acum vom crește numărul de sarcini la 100 și, după ce vom trimite 100 de sarcini, vom apela metoda awaitTermination(11, SECONDS) . Trecem un număr și o unitate de timp ca argumente. Această metodă va bloca firul principal timp de 11 secunde. Apoi vom apela shutdownNow() pentru a forța ExecutorService să se închidă fără a aștepta finalizarea tuturor sarcinilor.

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

La sfârșit, vom afișa informații despre starea executorService .

Iată rezultatul consolei pe care îl obținem:

Solicitarea utilizatorului procesată #0 pe firul pool-1-thread-1
Cererea utilizatorului procesată #2 pe firul pool-1-thread-3
Cererea utilizatorului procesată #1 pe firul pool-1-thread-2
Cererea utilizatorului procesată #4 pe pool- Fir 1-thread-3
Cererea utilizatorului procesată #5 pe firul pool-1-thread-2
Cererea utilizatorului procesată #3 pe firul pool-1-thread-1
Cererea utilizatorului procesată #6 pe firul pool-1-thread-3
Utilizator procesat cererea #7 pe firul pool-1-thread-2
Cererea utilizatorului procesată #8 pe firul pool-1-thread-1
Cererea utilizatorului procesată #9 pe firul pool-1-thread-3
Cererea utilizatorului procesată #11 pe pool-1- thread-1 fir
Cererea utilizatorului procesată #10 pe firul pool-1-thread-2
Solicitarea utilizatorului procesată #13 pe firul pool-1-thread-1
Solicitarea utilizatorului #14 procesată pe firul pool-1-thread-2
Solicitarea utilizatorului #12 procesată pe firul pool-1-thread-3
java.util.concurrent.ThreadPoolExecutor@452b3a41[Închidere, dimensiunea pool-ului = 3, fire active = 3 , sarcini puse în coadă = 0, sarcini finalizate = 15]
Cererea utilizatorului procesată #17 pe firul pool-1-thread-3
Cererea utilizatorului procesată #15 pe firul pool-1-thread-1
Cererea utilizatorului procesată #16 pe firul pool-1-thread -2 fire

Acesta este urmat de 3 InterruptedExceptions , aruncate de metodele de somn din 3 sarcini active.

Putem vedea că atunci când programul se termină, sunt efectuate 15 sarcini, dar pool-ul avea încă 3 fire active care nu și-au terminat de executat sarcinile. Metoda interrupt() este apelată pe aceste trei fire de execuție, ceea ce înseamnă că sarcina se va finaliza, dar în cazul nostru, metoda sleep lansează o excepție InterruptedException . Vedem, de asemenea, că după ce metoda shutdownNow() este apelată, coada de activități este ștearsă.

Deci, atunci când utilizați un ExecutorService cu un număr fix de fire în pool, asigurați-vă că vă amintiți cum funcționează. Acest tip este potrivit pentru sarcini cu o sarcină constantă cunoscută.

Iată o altă întrebare interesantă: dacă trebuie să utilizați un executor pentru un singur fir, ce metodă ar trebui să apelați? newSingleThreadExecutor() sau newFixedThreadPool(1) ?

Ambii executori vor avea un comportament echivalent. Singura diferență este că metoda newSingleThreadExecutor() va returna un executant care nu poate fi reconfigurat ulterior pentru a utiliza fire suplimentare.