Zašto Alibaba zabranjuje kreiranje bazena niti preko Executors?
U Alibabinom priručniku za razvoj, u delu o konkurentnom programiranju, stoji: bazeni niti ne smeju se kreirati preko Executors, već preko ThreadPoolExecutor. Danas ćemo analizom izvornog koda objasniti razlog zabrane.

Čitanjem ovog članka saznaćete:
- definiciju bazena niti
- nekoliko načina kreiranja bazena niti preko Executors
- objekat ThreadPoolExecutor
- odnos između logike izvršenja zadataka bazena niti i parametara bazena
- Executors metode koje vraćaju objekat ThreadPoolExecutor
- test OOM izuzetka
- kako definisati parametre bazena niti
Ako vas zanima samo uzrok, možete preći direktno na zaključak na kraju.
1. Definicija bazena niti
Upravlja skupom radnih niti. Ponovno korišćenje niti preko bazena niti ima sledeće prednosti:
- Smanjenje troškova stvaranja resursa => smanjenje memorijskih troškova, jer stvaranje niti zauzima memoriju.
- Smanjenje sistemskih troškova => stvaranje niti zahteva vreme, što usporava obradu zahteva.
- Povećanje stabilnosti => izbegava se OutOfMemoryError (skraćeno OOM) izazvan neograničenim stvaranjem niti.
Načini kreiranja bazena niti preko Executors, prema tipu objekta koji vraćaju, dele se u tri kategorije:
- kreiranje koje vraća objekat ThreadPoolExecutor
- kreiranje koje vraća objekat ScheduleThreadPoolExecutor
- kreiranje koje vraća objekat ForkJoinPool
Ovaj članak razmatra samo kreiranje koje vraća objekat ThreadPoolExecutor.
2. Objekat ThreadPoolExecutor
Pre nego što predstavimo načine kreiranja bazena niti preko Executors, predstavimo ThreadPoolExecutor, jer sve te statičke metode kreiranja vraćaju objekat ThreadPoolExecutor; razlika u odnosu na ručno kreiranje ThreadPoolExecutor objekta jeste u tome što ne moramo sami da prosleđujemo parametre konstruktora.
ThreadPoolExecutor ima četiri konstruktora, ali svi na kraju pozivaju isti:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)Objašnjenje parametara konstruktora:
- corePoolSize => broj jezgarnih niti bazena niti
- maximumPoolSize => maksimalni broj niti bazena
- keepAliveTime => vreme života niti u mirovanju
- unit => jedinica vremena
- workQueue => bafer red koji koristi bazen niti
- threadFactory => fabrika koju bazen niti koristi za stvaranje niti
- handler => strategija bazena niti za odbijene zadatke
Odnos između logike izvršenja zadataka bazena niti i parametara bazena.

Objašnjenje logike izvršenja:
- proverava se da li je broj jezgarnih niti popunjen; veličina jezgarnih niti zavisi od parametra corePoolSize; ako nije popunjen, kreira se nit za izvršenje zadatka;
- ako je bazen jezgarnih niti popunjen, proverava se da li je red pun; popunjenost reda zavisi od parametra workQueue; ako nije pun, zadatak se dodaje u red;
- ako je red pun, proverava se da li je bazen niti popunjen; popunjenost bazena zavisi od parametra maximumPoolSize; ako nije popunjen, kreira se nit za izvršenje zadatka;
- ako je bazen niti popunjen, za zadatke koji se ne mogu izvršiti primenjuje se strategija odbijanja, koja zavisi od parametra handler.
Executors metode koje vraćaju objekat ThreadPoolExecutor ukupno su tri:
- Executors.newCachedThreadPool => kreira keširajući bazen niti
- Executors#.newSingleThreadExecutor => kreira jednonitni bazen niti
- Executors.newFixedThreadPool => kreira bazen niti fiksne dužine
Metod Executors.newCachedThreadPool:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}CachedThreadPool je bazen niti koji po potrebi kreira nove niti:
- corePoolSize => 0, broj jezgarnih niti je 0
- maximumPoolSize => Integer.MAX_VALUE, maksimalni broj niti je Integer.MAX_VALUE, što se može shvatiti kao da se niti mogu neograničeno stvarati
- keepAliveTime => 60L
- unit => sekunde
- workQueue => SynchronousQueue
Kada se podnese zadatak, pošto je corePoolSize 0 ne stvara se jezgrana nit; SynchronousQueue je red koji ne čuva elemente, što se može shvatiti kao da je red uvek pun, pa će se za izvršenje zadatka na kraju stvoriti nejezgrana nit.
Nejezgrana nit po isteku 60 s mirovanja se povraća. Pošto je Integer.MAX_VALUE vrlo veliki, može se smatrati da niti mogu neograničeno da se stvaraju; u uslovima ograničenih resursa lako dovodi do OOM izuzetka.
Metod Executors.newSingleThreadExecutor:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}SingleThreadExecutor je jednonitni bazen niti, sa samo jednom jezgranom niti:
- corePoolSize => 1, broj jezgarnih niti je 1
- maximumPoolSize => 1, maksimalni broj niti je 1, tj. može se stvoriti najviše jedna nit — jedina nit je i jezgrana
- keepAliveTime => 0L
- unit => milisekunde
- workQueue => LinkedBlockingQueue
Kada se podnese zadatak, prvo se stvara jedna jezgrana nit za izvršenje zadatka; ako se premaši broj jezgarnih niti, zadatak se stavlja u red; pošto je LinkedBlockingQueue red dužine Integer.MAX_VALUE, može se smatrati neograničenim, pa se u red može staviti neograničeno mnogo zadataka; u uslovima ograničenih resursa lako dovodi do OOM izuzetka, a pošto je red neograničen, parametri maximumPoolSize i keepAliveTime postaju nevažeći — nejezgrane niti se uopšte neće stvarati.
Metod Executors.newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}FixedThreadPool je bazen niti sa fiksnim brojem jezgarnih niti, čiji broj zadaje korisnik:
- corePoolSize => nThreads, broj jezgarnih niti je 1
- maximumPoolSize => nThreads, maksimalni broj niti je nThreads, tj. može se stvoriti najviše nThreads niti
- keepAliveTime => 0L
- unit => milisekunde
- workQueue => LinkedBlockingQueue
Sličan je SingleThreadExecutor-u; jedina razlika je u broju jezgarnih niti, a pošto koristi LinkedBlockingQueue, u uslovima ograničenih resursa lako dovodi do OOM izuzetka.
3. Zaključak:
- FixedThreadPool i SingleThreadExecutor => dozvoljena dužina reda zahteva je Integer.MAX_VALUE, što može dovesti do gomilanja velikog broja zahteva i izazvati OOM izuzetak;
- CachedThreadPool => dozvoljeni broj niti je Integer.MAX_VALUE, što može dovesti do stvaranja velikog broja niti i izazvati OOM izuzetak.
Zato se zabranjuje kreiranje bazena niti preko Executors, a preporučuje se da se ThreadPoolExecutor kreira samostalno.
Test OOM izuzetka
Teorijski se može pojaviti OOM izuzetak, pa ga moramo testirati radi potvrde.
Test klasa: TaskTest.java
public class TaskTest {
public static void main(String[] args) {
ExecutorService es = Executors.newCachedThreadPool();
int i = 0;
while (true) {
es.submit(new Task(i++));
}
}
}Preko Executors-a kreira se CachedThreadPool kojem se neograničeno dodaju niti.
Pre pokretanja test klase, smanjite JVM memoriju, inače lako može doći do problema na računaru [ne pitajte me kako znam — iskustvo je!]. U IDEA-i: Run -> Edit Configurations.

Objašnjenje JVM parametara:
- -Xms10M => početna vrednost Java Heap memorije
- -Xmx10M => maksimalna vrednost Java Heap memorije
Rezultat izvršavanja:
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
Disconnected from the target VM, address: '127.0.0.1:60416', transport: 'socket'Kada se krene preko 30 000 niti, počinje OOM greška.
Ostala dva bazena niti se ne testiraju; metoda testiranja je ista, samo se razlikuje koji bazen niti se kreira.
4. Kako definisati parametre bazena niti
1) CPU-intenzivno
Veličina bazena niti se preporučuje da bude broj jezgara CPU-a + 1; broj jezgara CPU-a može se dobiti metodom Runtime.availableProcessors.
2) I/O-intenzivno
Broj jezgara CPU-a * iskorišćenost CPU-a * (1 + vreme čekanja niti / CPU vreme niti).
3) Mešovito
Zadaci se podele na CPU-intenzivne i I/O-intenzivne, pa se zatim obrade različitim bazenima niti, tako da svaki bazen može da se prilagodi svom opterećenju.
4) Blokirajući red
Preporučuje se korišćenje ograničenog reda; ograničeni red pomaže u izbegavanju iscrpljivanja resursa.
5) Strategija odbijanja
Podrazumevano se koristi strategija odbijanja AbortPolicy, koja u programu direktno izbacuje RejectedExecutionException [pošto je to izuzetak u izvršavanju, catch nije obavezan]; ovakav način obrade nije pretjerano elegantan.
Od strategija odbijanja, sledeće se preporučuju:
- U programu se hvata RejectedExecutionException i u obradi izuzetka radi sa zadatkom. Za podrazumevanu strategiju odbijanja.
- Koristi se strategija odbijanja CallerRunsPolicy, koja zadatak dodeljuje niti koja je pozvala execute [obično glavnoj niti]; tada glavna nit izvesno vreme ne može podnositi nijedan zadatak, čime se radnim nitima daje vremena da obrade zadatke u toku. Podneti zadaci će se čuvati u TCP redu; pun TCP red uticaće na klijente — to je postepeno smanjenje performansi.
- Prilagođena strategija odbijanja, dovoljno je implementirati interfejs RejectedExecutionHandler.
- Ako zadaci nisu od posebnog značaja, odbacivanje zadataka preko strategija DiscardPolicy i DiscardOldestPolicy je takođe prihvatljivo.
Ako se preko statičkih metoda Executors-a kreira objekat ThreadPoolExecutor, upotrebom Semaphore-a može se ograničiti protok izvršenja zadataka, što takođe izbegava OOM izuzetak.
Pošto iskustva u definisanju parametara bazena niti nema previše, sve je teorijsko; dobrodošli doprinosi iskusnih stručnjaka u komentarima.
Urednik: Marko Marković Link preuzimanja: Originalni link: https://juejin.cn/post/6844903989675458574
Na GitHub-u je konačno stiglo prvo izdanje PDF-aopen-source baze znanja sa preko 17000 zvezdica „Napredni put ka naprednom Javom“! Obuhvata osnove sintakse Java-e, nizove i stringove, OOP, okvir kolekcija, Java IO, obradu izuzetaka, nove osobine Java-e, mrežno programiranje, NIO, konkurentno programiranje, JVM itd. — ukupno preko 320 000 reči i preko 500 ručno nacrtanih ilustracija, što se može opisati kao pristupačno i sa humorom... Više detalja: Odlično, Java tutorial sa preko 17000 zvezdica na GitHub-u
