Vodič za uslugu Java ExecutorService

1. Pregled

ExecutorService je okvir koji pruža JDK koji pojednostavljuje izvršavanje zadataka u asinkronom načinu. Općenito govoreći, ExecutorService automatski pruža skup niti i API za dodjeljivanje zadataka.

2. Instanciranje ExecutorService

2.1. Tvorničke metode Izvršitelji Razred

Najlakši način za stvaranje ExecutorService je korištenje jedne od tvorničkih metoda Izvršitelji razred.

Na primjer, sljedeći redak koda stvorit će spremište niti s 10 niti:

ExecutorService izvršitelj = Executors.newFixedThreadPool (10);

Postoji nekoliko drugih tvorničkih metoda za stvaranje unaprijed definiranih ExecutorService koji odgovaraju određenim slučajevima upotrebe. Da biste pronašli najbolji način za svoje potrebe, pogledajte službenu dokumentaciju tvrtke Oracle.

2.2. Izravno stvorite ExecutorService

Jer ExecutorService je sučelje, može se koristiti instanca bilo koje njegove implementacije. Postoji nekoliko implementacija koje možete odabrati u java.util.concurrent paket ili možete stvoriti vlastiti.

Na primjer, ThreadPoolExeecuter class ima nekoliko konstruktora koji se mogu koristiti za konfiguriranje izvršne usluge i njezinog unutarnjeg spremišta.

ExecutorService executorService = novi ThreadPoolExecutor (1, 1, 0L, TimeUnit.MILLISECONDS, novi LinkedBlockingQueue ());

Možda ćete primijetiti da je gornji kod vrlo sličan izvornom kodu tvorničke metode newSingleThreadExecutor (). U većini slučajeva detaljna ručna konfiguracija nije potrebna.

3. Dodjeljivanje zadataka ExecutorService

ExecutorService može izvršiti Izvodljivo i Pozivno zadaci. Da bi stvari bile jednostavnije u ovom članku, koristit će se dva primitivna zadatka. Primijetite da se ovdje koriste lambda izrazi umjesto anonimnih unutarnjih klasa:

Izvodljivo runnableTask = () -> {try {TimeUnit.MILLISECONDS.sleep (300); } catch (InterruptedException e) {e.printStackTrace (); }}; Callable callableTask = () -> {TimeUnit.MILLISECONDS.sleep (300); return "Izvršenje zadatka"; }; Popis callableTasks = novi ArrayList (); callableTasks.add (callableTask); callableTasks.add (callableTask); callableTasks.add (callableTask);

Zadaci se mogu dodijeliti ExecutorService koristeći nekoliko metoda, uključujući izvršiti(), koja je naslijeđena od Izvršitelj sučelje, a također podnijeti(), invokeAny (), invokeAll ().

The izvršiti() metoda je poništiti, i ne daje nikakvu mogućnost dobivanja rezultata izvršenja zadatka ili provjere statusa zadatka (izvodi li se ili se izvršava).

executorService.execute (runnableTask);

podnijeti() podnosi a Pozivno ili a Izvodljivo zadatak an ExecutorService i vraća rezultat tipa Budućnost.

Buduća budućnost = executorService.submit (callableTask);

invokeAny () dodjeljuje zbirku zadataka sustavu ExecutorService, uzrokujući izvršenje svakog i vraća rezultat uspješnog izvršavanja jednog zadatka (ako je uspješno izvršeno).

Rezultat niza = executorService.invokeAny (callableTasks);

invokeAll () dodjeljuje zbirku zadataka sustavu ExecutorService, uzrokujući da se svako izvrši i vraća rezultat svih izvršavanja zadataka u obliku popisa objekata tipa Budućnost.

Popis futures = executorService.invokeAll (callableTasks);

Prije daljnjeg razgovora, potrebno je razgovarati o još dvije stvari: gašenju ExecutorService i rješavanje Budućnost povratne vrste.

4. Isključivanje an ExecutorService

Općenito, ExecutorService neće se automatski uništiti kada nema zadatka za obradu. Ostat će živ i čekati novi posao.

U nekim slučajevima ovo je vrlo korisno; na primjer, ako aplikacija treba obrađivati ​​zadatke koji se pojavljuju na neredovit način ili količina tih zadataka nije poznata u vrijeme sastavljanja.

S druge strane, aplikacija bi mogla doći do svog kraja, ali neće biti zaustavljena jer čeka ExecutorService uzrokovat će da JVM nastavi raditi.

Da biste pravilno isključili ExecutorService, imamo ugasiti() i shutdownNow () Apis.

The ugasiti()metoda ne uzrokuje trenutno uništavanje ExecutorService. To će učiniti ExecutorService prestanite prihvaćati nove zadatke i isključite se nakon što sve pokrenute niti završe svoj trenutni posao.

executorService.shutdown ();

The shutdownNow () metoda pokušava uništiti ExecutorService odmah, ali ne garantira da će se sve pokrenute niti istovremeno zaustaviti. Ova metoda vraća popis zadataka koji čekaju na obradu. Na programeru je odluka što će učiniti s tim zadacima.

Popis notExecutedTasks = executorService.shutDownNow ();

Jedan dobar način za isključivanje ExecutorService (što Oracle također preporučuje) je korištenje obje ove metode u kombinaciji s awaitTermination () metoda. Ovim pristupom, ExecutorService prvo će prestati uzimati nove zadatke, a zatim pričekati određeno vrijeme da se svi zadaci dovrše. Ako to vrijeme istekne, izvršenje se odmah zaustavlja:

executorService.shutdown (); probajte {if (! executorService.awaitTermination (800, TimeUnit.MILLISECONDS)) {executorService.shutdownNow (); }} catch (InterruptedException e) {executorService.shutdownNow (); }

5. The Budućnost Sučelje

The podnijeti() i invokeAll () metode vraćaju objekt ili kolekciju predmeta tipa Budućnost, koji nam omogućuje dobivanje rezultata izvršenja zadatka ili provjeru statusa zadatka (izvodi li se ili se izvršava).

The Budućnost sučelje pruža posebnu metodu blokiranja dobiti() koji vraća stvarni rezultat Pozivno izvršenje zadatka ili null u slučaju Izvodljivo zadatak. Pozivanje dobiti() metoda dok se zadatak još uvijek izvodi izvršit će blokadu dok se zadatak pravilno ne izvrši i rezultat ne bude dostupan.

Buduća budućnost = executorService.submit (callableTask); Rezultat niza = null; pokušajte {rezultat = budućnost.get (); } catch (InterruptedException | ExecutionException e) {e.printStackTrace (); }

Uz vrlo dugo blokiranje uzrokovano dobiti() metodom, izvedba aplikacije može se pogoršati. Ako dobiveni podaci nisu presudni, moguće je izbjeći takav problem korištenjem vremenskih ograničenja:

Rezultat niza = future.get (200, TimeUnit.MILLISECONDS);

Ako je razdoblje izvršenja dulje od navedenog (u ovom slučaju 200 milisekundi), a TimeoutException bit će bačen.

The Gotovo je() metodom se može provjeriti je li dodijeljeni zadatak već obrađen ili nije.

The Budućnost sučelje također omogućava otkazivanje izvršenja zadatka s otkazati() metodu i provjeriti otkazivanje s isCancelled () metoda:

logička vrijednost otkazana = future.cancel (true); boolean isCancelled = future.isCancelled ();

6. The ScheduledExecutorService Sučelje

The ScheduledExecutorService izvodi zadatke nakon nekog unaprijed definiranog kašnjenja i / ili povremeno. Još jednom, najbolji način instanciranja a ScheduledExecutorService je korištenje tvorničkih metoda Izvršitelji razred.

Za ovaj odjeljak, a ScheduledExecutorService s jednom niti će se koristiti:

ScheduledExecutorService izvršiteljService = Izvršitelji .newSingleThreadScheduledExecutor ();

Za planiranje izvršenja jednog zadatka nakon određenog kašnjenja, koristimo zakazano () metoda ScheduledExecutorService. Postoje dva zakazano () metode koje omogućuju izvršavanje Izvodljivo ili Pozivno zadaci:

Budući rezultatFuture = executorService.schedule (callableTask, 1, TimeUnit.SECONDS);

The scheduleAtFixedRate () metoda omogućuje povremeno izvršavanje zadatka nakon određenog kašnjenja. Gornji kod kasni jednu sekundu prije izvršenja callableTask.

Sljedeći blok koda izvršit će zadatak nakon početnog kašnjenja od 100 milisekundi, a nakon toga izvršit će isti zadatak svakih 450 milisekundi. Ako procesoru treba više vremena za izvršavanje dodijeljenog zadatka od razdoblje parametar scheduleAtFixedRate () metoda, ScheduledExecutorService pričekat će dok se trenutni zadatak ne dovrši prije početka sljedećeg:

Budući rezultatFuture = service .scheduleAtFixedRate (runnableTask, 100, 450, TimeUnit.MILLISECONDS);

Ako je potrebno imati fiksno kašnjenje između ponavljanja zadatka, scheduleWithFixedDelay () trebalo bi se koristiti. Na primjer, sljedeći će kôd zajamčiti 150-milisekundnu stanku između kraja trenutnog izvršavanja i početka drugog.

service.scheduleWithFixedDelay (zadatak, 100, 150, TimeUnit.MILLISECONDS);

Prema scheduleAtFixedRate () i scheduleWithFixedDelay () metode ugovora, razdoblje izvršenja zadatka završit će prestankom ExecutorService ili ako se tijekom izvršavanja zadatka izuzme izuzetak.

7. ExecutorService nasuprot Fork / Join

Nakon izlaska Jave 7, mnogi su programeri odlučili da ExecutorService okvir treba zamijeniti okvirom fork / join. Međutim, ovo nije uvijek ispravna odluka. Unatoč jednostavnosti korištenja i čestim poboljšanjima u izvedbi povezanim s račvanjem / spajanjem, također se smanjuje količina nadzora programera nad istodobnim izvršavanjem.

ExecutorService daje programeru mogućnost kontrole broja generiranih niti i granularnosti zadataka koje bi trebale izvršavati zasebne niti. Najbolji slučaj upotrebe za ExecutorService je obrada neovisnih zadataka, poput transakcija ili zahtjeva prema shemi "jedna nit za jedan zadatak".

Suprotno tome, prema Oracleovoj dokumentaciji, fork / join je dizajniran da ubrza rad koji se rekurzivno može razbiti na manje dijelove.

8. Zaključak

Čak i unatoč relativnoj jednostavnosti ExecutorService, postoji nekoliko uobičajenih zamki. Sažeto ćemo ih:

Zadržavanje neiskorištenog ExecutorService živ: U odjeljku 4 ovog članka nalazi se detaljno objašnjenje o tome kako isključiti ExecutorService;

Pogrešan kapacitet spremišta niti pri korištenju spremišta niti fiksne duljine: Vrlo je važno odrediti koliko niti će aplikaciji trebati za učinkovito izvršavanje zadataka. Prevelik bazen niti će uzrokovati nepotrebne režijske troškove samo da bi stvorio niti koje će uglavnom biti u načinu čekanja. Premalo aplikacija može izgledati kao da ne reagira zbog dugih perioda čekanja na zadatke u redu;

Pozivanje a Budućnost‘S dobiti() metoda nakon otkazivanja zadatka: Pokušaj dobivanja rezultata već otkazanog zadatka pokrenut će a CancellationException.

Neočekivano dugo blokiranje sa Budućnost‘S dobiti() metoda: Treba iskoristiti vremenska ograničenja kako bi se izbjegla neočekivana čekanja.

Kôd za ovaj članak dostupan je u spremištu GitHub.