Hogyan indítsunk el egy szálat a Java-ban

1. Bemutatkozás

Ebben az oktatóanyagban különböző módokat fogunk felfedezni egy szál elindításához és párhuzamos feladatok végrehajtásához.

Ez nagyon hasznos, különösen olyan hosszú vagy ismétlődő műveletek esetén, amelyek nem futtathatók a fő szálon, vagy ahol a felhasználói felület interakcióját nem lehet várakozni a művelet eredményeire várva.

Ha többet szeretne megtudni a szálak részleteiről, feltétlenül olvassa el a Java szálak életciklusáról szóló oktatóanyagunkat.

2. A szál futtatásának alapjai

Könnyedén megírhatunk néhány logikát, amely párhuzamos szálban fut, a cérna keretrendszer.

Próbálkozzunk egy alapvető példával a cérna osztály:

public class A NewThread kiterjeszti a Threadot {public void run () {long startTime = System.currentTimeMillis (); int i = 0; while (true) {System.out.println (this.getName () + ": Új szál fut ..." + i ++); próbáld meg {// Várjon egy másodpercet, hogy ne nyomtasson túl gyorsan Thread.sleep (1000); } catch (InterruptedException e) {e.printStackTrace (); } ...}}}

És most írunk egy második osztályt a szál inicializálásához és elindításához:

public class SingleThreadExample {public static void main (String [] args) {NewThread t = new NewThread (); t.start (); }}

Fel kellene hívnunk a Rajt() módszer a ÚJ állapot (a meg nem indult egyenértékű). Ellenkező esetben a Java dob egy példányt IllegalThreadStateException kivétel.

Tegyük fel, hogy több szálat kell elindítanunk:

public class MultipleThreadsExample {public static void main (String [] args) {NewThread t1 = new NewThread (); t1.setName ("MyThread-1"); NewThread t2 = new NewThread (); t2.setName ("MyThread-2"); t1.start (); t2.start (); }}

Kódunk még mindig nagyon egyszerűnek tűnik, és nagyon hasonlít az online példákhoz.

Természetesen, ez messze nem a gyártásra kész kód, ahol kritikus fontosságú az erőforrások helyes kezelése, a túl sok kontextusváltás vagy a túl sok memóriahasználat elkerülése érdekében.

Tehát, hogy készen álljunk a gyártásra, most meg kell írnunk egy kiegészítő kazántáblát foglalkozni vele:

  • új szálak következetes létrehozása
  • az egyidejűleg élő szálak száma
  • a szálak elosztása: nagyon fontos a démonláncok számára a szivárgások elkerülése érdekében

Ha akarjuk, megírhatjuk a saját kódunkat ezekhez az esetekhez és még néhányhoz, de miért kellene újra feltalálni a kereket?

3. Az ExecutorService Keretrendszer

A ExecutorService megvalósítja a szálkészlet tervezési mintáját (más néven replikált munkásnak vagy munkás-legénység modellnek), és gondoskodik a fent említett szálkezelésről, ráadásul néhány nagyon hasznos funkcióval egészíti ki, mint például a szál újrafelhasználhatósága és a feladatsorok.

Különösen a szál újrafelhasználhatósága nagyon fontos: egy nagyszabású alkalmazásban sok szál objektum allokálása és elosztása jelentős memóriakezelési költségeket eredményez.

Munkásszálakkal minimalizáljuk a szál létrehozása által okozott általános költségeket.

A medence konfigurációjának megkönnyítése érdekében ExecutorService könnyű konstruktorral és néhány testreszabási lehetőséggel érkezik, mint például a sor típusa, a szálak minimális és maximális száma, valamint elnevezési szokásuk.

További részletek a ExecutorService, kérjük, olvassa el a Java ExecutorService útmutatót.

4. Feladat indítása végrehajtókkal

Ennek az erőteljes keretrendszernek köszönhetően átállhatjuk gondolkodásmódunkat a kezdő szálakról a feladatok benyújtására.

Nézzük meg, hogyan nyújthatunk be aszinkron feladatot a végrehajtónknak:

ExecutorService végrehajtó = Executors.newFixedThreadPool (10); ... végrehajtó.submit (() -> {új Feladat ();});

Kétféle módszert használhatunk: végrehajtani, amely semmit sem ad vissza, és Beküldés, amely a Jövő összefoglalja a számítás eredményét.

További információ a Határidős, kérjük, olvassa el a java.util.concurrent.Future útmutatónkat.

5. Feladat indítása CompletableFutures

A végeredmény lekérése a Jövő objektumot használhatjuk kap Az objektumban elérhető módszer, de ez blokkolja a szülőszálat a számítás végéig.

Alternatív megoldásként elkerülhetjük a blokkot azzal, hogy több logikát adunk a feladatunkhoz, de növelnünk kell a kódunk összetettségét.

A Java 1.8 új keretrendszert vezetett be a Jövő a számítás eredményének jobb működése érdekében: a CompletableFuture.

CompletableFuture megvalósítja CompletableStage, amely hatalmas választékot kínál a visszahívások csatolásához, és elkerüli az összes olyan vízvezeték-szerelést, amely szükséges a műveletek futtatásához az eredményen, miután elkészült.

A feladat elküldéséhez sokkal egyszerűbb a megvalósítás:

CompletableFuture.supplyAsync (() -> "Hello");

supplyAsync vesz egy Támogató a kódot tartalmazza, amelyet aszinkron módon akarunk végrehajtani - esetünkben a lambda paramétert.

A feladatot most implicit módon elküldtük a ForkJoinPool.commonPool (), vagy megadhatjuk a Végrehajtó második paraméterként előnyben részesítjük.

Hogy többet tudjon meg CompletableFuture, kérjük, olvassa el a CompletableFuture útmutatónkat.

6. Késleltetett vagy időszakos feladatok futtatása

Ha összetett webalkalmazásokkal dolgozunk, akkor előfordulhat, hogy feladatokat kell futtatnunk meghatározott időpontokban, esetleg rendszeresen.

A Java-nak kevés olyan eszköze van, amely segít késleltetett vagy ismétlődő műveletek futtatásában:

  • java.util.Timer
  • java.util.concurrent.ScheduledThreadPoolExecutor

6.1. Időzítő

Időzítő egy olyan lehetőség, amellyel a feladatokat a jövőbeli végrehajtáshoz háttérszálon lehet ütemezni.

A feladatok ütemezhetők egyszeri végrehajtásra, vagy rendszeres időközönként ismételt végrehajtásra.

Lássuk, hogy néz ki a kód, ha egy másodperc késés után akarunk futtatni egy feladatot:

TimerTask task = new TimerTask () {public void run () {System.out.println ("A feladat végrehajtása:" + új dátum () + "n" + "A szál neve:" + Thread.currentThread (). GetName ( )); }}; Időzítő időzítő = új időzítő ("Időzítő"); hosszú késés = 1000L; timer.schedule (feladat, késés);

Most adjunk hozzá egy ismétlődő ütemtervet:

timer.scheduleAtFixedRate (repeatTask, delay, period);

Ezúttal a feladat a megadott késés után fog futni, és az eltelt idő után ismétlődik.

További információkért olvassa el a Java Timer útmutatót.

6.2. ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor hasonló módszerekkel rendelkezik Időzítő osztály:

ScheduledExecutorService executorService = Executors.newScheduledThreadPool (2); ScheduledFuture resultFuture = végrehajtóSzolgáltatás.schedule (callableTask, 1, TimeUnit.SECONDS);

Példánk végére használjuk scheduleAtFixedRate () ismétlődő feladatokhoz:

ScheduledFuture resultFuture = végrehajtóService.scheduleAtFixedRate (runnableTask, 100, 450, TimeUnit.MILLISECONDS);

A fenti kód egy 100 milliszekundumos kezdeti késés után hajt végre egy feladatot, és ezt követően ugyanazt a feladatot hajtja végre minden 450 milliszekundumban.

Ha a processzor nem tudja befejezni a feladat feldolgozását időben a következő előfordulás előtt, akkor a ScheduledExecutorService a következő feladat megkezdése előtt megvárja az aktuális feladat befejezését.

Ennek a várakozási időnek a elkerülése érdekében használhatjuk scheduleWithFixedDelay (), amely a nevével leírva rögzített hosszúságú késést garantál a feladat ismétlései között.

További részletek a ScheduledExecutorService, kérjük, olvassa el a Java ExecutorService útmutatót.

6.3. Melyik eszköz jobb?

Ha a fenti példákat futtatjuk, a számítás eredménye ugyanaz lesz.

Így, hogyan válasszuk ki a megfelelő eszközt?

Ha egy keretrendszer több választási lehetőséget kínál, fontos megérteni a mögöttes technológiát, hogy megalapozott döntést hozzon.

Próbáljunk meg merülni egy kicsit mélyebben a motorháztető alatt.

Időzítő:

  • nem kínál valós idejű garanciákat: a feladatokat ütemezi a Object.wait (hosszú) módszer
  • egyetlen háttérszál van, így a feladatok egymás után futnak, és egy hosszú ideje futó feladat késleltetheti másokat
  • futásidejű kivételeket dobtak be a TimerTask megöli az egyetlen elérhető szálat, így megöli Időzítő

ScheduledThreadPoolExecutor:

  • tetszőleges számú szálral konfigurálható
  • kihasználhatja az összes rendelkezésre álló CPU magot
  • futásidejű kivételeket fog meg, és lehetővé teszi, hogy kezeljük őket, ha akarjuk (felülírva afterExecute módszer től ThreadPoolExecutor)
  • törli a kivételt eldöntő feladatot, miközben hagyja, hogy mások tovább futjanak
  • az operációs rendszer ütemezési rendszerére támaszkodik az időzónák, a késések, a szoláris idő stb.
  • együttműködési API-t biztosít, ha több feladat közötti koordinációra van szükségünk, például várni kell az összes beküldött feladat befejezésére
  • jobb API-t biztosít a szál életciklusának kezeléséhez

A választás most kézenfekvő, igaz?

7. Különbség Jövő és ScheduledFuture

Kódpéldáinkban ezt megfigyelhetjük ScheduledThreadPoolExecutor egy adott típusú Jövő: ScheduledFuture.

ScheduledFuture kiterjeszti mindkettőt Jövő és Késleltetett interfészek, így örökölve a további módszert getDelay amely visszaadja az aktuális feladathoz társított fennmaradó késleltetést. Meghosszabbította RunnableScheduledFuture amely hozzáad egy módszert annak ellenőrzésére, hogy a feladat időszakos-e.

ScheduledThreadPoolExecutor ezeket a konstrukciókat a belső osztályon keresztül valósítja meg ScheduledFutureTask és felhasználja őket a feladat életciklusának irányítására.

8. Következtetések

Ebben az oktatóanyagban kísérleteztünk a rendelkezésre álló különböző keretekkel a szálak indításához és a feladatok párhuzamos futtatásához.

Aztán elmélyültünk a különbségek között Időzítő és ScheduledThreadPoolExecutor.

A cikk forráskódja elérhető a GitHubon.