Un alt tip de pool de fire este „în cache”. Astfel de piscine de fire sunt la fel de frecvent utilizate ca și cele fixe.

După cum este indicat de nume, acest tip de pool de fire memorează în cache firele. Păstrează firele neutilizate în viață pentru o perioadă limitată de timp pentru a le reutiliza pentru a efectua sarcini noi. Un astfel de grup de fire este cel mai bun pentru atunci când avem o cantitate rezonabilă de muncă ușoară.

Semnificația „o sumă rezonabilă” este destul de largă, dar ar trebui să știți că o astfel de piscină nu este potrivită pentru fiecare număr de sarcini. De exemplu, să presupunem că vrem să creăm un milion de sarcini. Chiar dacă fiecare necesită o perioadă foarte mică de timp, vom folosi totuși o cantitate nerezonabilă de resurse și vom degrada performanța. De asemenea, ar trebui să evităm astfel de pool-uri atunci când timpul de execuție este imprevizibil, de exemplu, cu sarcini I/O.

Sub capotă, constructorul ThreadPoolExecutor este apelat cu următoarele argumente:


public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, 
      new SynchronousQueue<Runnable>());
}

Următoarele valori sunt transmise constructorului ca argumente:

Parametru Valoare
corePoolSize (cate fire vor fi gata (pornite) când pornește serviciul executor ) 0
maximumPoolSize (numărul maxim de fire de execuție pe care le poate crea un serviciu executor ) Număr întreg.MAX_VALUE
keepAliveTime (timpul în care un fir eliberat va continua să trăiască înainte de a fi distrus dacă numărul de fire este mai mare decât corePoolSize ) 60L
unitate (unități de timp) TimeUnit.SECONDS
workQueue (implementarea unei cozi) nou SynchronousQueue<Runnable>()

Și putem trece propria noastră implementare a ThreadFactory exact în același mod.

Să vorbim despre SynchronousQueue

Ideea de bază a unui transfer sincron este destul de simplă și totuși contra-intuitivă (adică intuiția sau bunul simț vă spune că este greșit): puteți adăuga un element la o coadă dacă și numai dacă un alt thread primește elementul la acelasi timp. Cu alte cuvinte, o coadă sincronă nu poate avea sarcini în ea, deoarece de îndată ce sosește o nouă sarcină, firul de execuție a preluat deja sarcina .

Când o nouă sarcină intră în coadă, dacă există un fir activ liber în pool, atunci acesta preia sarcina. Dacă toate firele sunt ocupate, atunci este creat un nou thread.

Un pool stocat în cache începe cu zero fire și poate crește la Integer.MAX_VALUE fire. În esență, dimensiunea unui pool de fire din cache este limitată doar de resursele sistemului.

Pentru a conserva resursele sistemului, pool-urile de fire din cache elimină firele care sunt inactive timp de un minut.

Să vedem cum funcționează în practică. Vom crea o clasă de sarcini care modelează o cerere de utilizator:


public class Task implements Runnable {
   int taskNumber;

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

   @Override
   public void run() {
       System.out.println("Processed user request #" + taskNumber + " on thread " + Thread.currentThread().getName());
   }
}
    

În metoda principală , creăm newCachedThreadPool și apoi adăugăm 3 sarcini pentru execuție. Aici tipărim starea serviciului nostru (1) .

Apoi, facem o pauză de 30 de secunde, începem o altă sarcină și afișăm starea (2) .

După aceea, întrerupem firul nostru principal timp de 70 de secunde, imprimăm starea (3) , apoi adăugăm din nou 3 sarcini și imprimăm din nou starea (4) .

În locurile în care afișăm starea imediat după adăugarea unei sarcini, mai întâi adăugăm un somn de 1 secundă pentru rezultate actualizate.


ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 3; i++) {
            executorService.submit(new Task(i));
        }
 
        TimeUnit.SECONDS.sleep(1);
            System.out.println(executorService);	//(1)
 
        TimeUnit.SECONDS.sleep(30);
 
        executorService.submit(new Task(3));
        TimeUnit.SECONDS.sleep(1);
            System.out.println(executorService);	//(2)
 
        TimeUnit.SECONDS.sleep(70);
 
            System.out.println(executorService);	//(3)
 
        for (int i = 4; i < 7; i++) {
            executorService.submit(new Task(i));
        }
 
        TimeUnit.SECONDS.sleep(1);
            System.out.println(executorService);	//(4)
        executorService.shutdown();

Și iată rezultatul:

Solicitarea utilizatorului #0 procesată pe firul pool-1-thread-1
Cererea utilizatorului #1 procesată pe firul pool-1-thread-2
Cererea utilizatorului #2 procesată pe firul pool-1-thread-3
(1) java.util.concurrent .ThreadPoolExecutor@f6f4d33[Rulează, dimensiunea pool-ului = 3, fire active = 0, sarcini puse în coadă = 0, sarcini finalizate = 3]
Solicitarea utilizatorului #3 procesată pe firul pool-1-thread-2
(2) java.util.concurrent. ThreadPoolExecutor@f6f4d33[Rulează, dimensiunea grupului = 3, fire active = 0, sarcini puse în coadă = 0, sarcini finalizate = 4] (3)
java.util.concurrent.ThreadPoolExecutor@f6f4d33[Rulare, dimensiune pool = 0, fire active = 0 , sarcini puse în coadă = 0, sarcini finalizate = 4]
Cererea utilizatorului #4 procesată pe firul pool-1-thread-4
Cererea utilizatorului procesată #5 pe firul pool-1-thread-5
Solicitarea utilizatorului #6 procesată pe firul pool-1-thread-4
(4) java.util.concurrent.ThreadPoolExecutor@f6f4d33[Running, pool size = 2, fire active = 0, coada tasks = 0, completeed tasks = 7]

Să trecem peste fiecare dintre pași:

Etapa Explicaţie
1 (după 3 sarcini finalizate) Am creat 3 fire și au fost executate 3 sarcini pe aceste trei fire.
Când se afișează starea, toate cele 3 sarcini sunt finalizate, iar firele sunt gata să efectueze alte sarcini.
2 (după pauză de 30 de secunde și executarea unei alte sarcini) După 30 de secunde de inactivitate, firele sunt încă vii și așteaptă sarcini.
O altă sarcină este adăugată și executată pe un fir de execuție preluat din pool-ul de fire de execuție rămase.
Nu a fost adăugat niciun fir nou în pool.
3 (după o pauză de 70 de secunde) Firele au fost scoase din bazin.
Nu există fire gata să accepte sarcini.
4 (după executarea a încă 3 sarcini) După ce au fost primite mai multe sarcini, au fost create fire noi. De data aceasta, doar două fire au reușit să proceseze 3 sarcini.

Ei bine, acum sunteți familiarizat cu logica unui alt tip de serviciu de executor.

Prin analogie cu alte metode din clasa de utilitate Executors , metoda newCachedThreadPool are și o versiune supraîncărcată care ia ca argument un obiect ThreadFactory .