Sådan startes en tråd i Java

1. Introduktion

I denne vejledning skal vi udforske forskellige måder at starte en tråd på og udføre parallelle opgaver.

Dette er meget nyttigt, især når det drejer sig om lange eller tilbagevendende operationer, der ikke kan køre på hovedtråden, eller hvor UI-interaktionen ikke kan holdes på vent, mens man venter på operationens resultater.

Hvis du vil lære mere om detaljerne i tråde, skal du helt sikkert læse vores vejledning om livets cyklus for en tråd i Java.

2. Grundlæggende om at køre en tråd

Vi kan nemt skrive nogle logik, der kører i en parallel tråd ved hjælp af Tråd ramme.

Lad os prøve et grundlæggende eksempel ved at udvide Tråd klasse:

offentlig klasse NewThread udvider tråd {public void run () {long startTime = System.currentTimeMillis (); int i = 0; while (true) {System.out.println (this.getName () + ": Ny tråd kører ..." + i ++); prøv {// Vent et sekund, så det ikke udskrives for hurtigt Thread.sleep (1000); } fange (InterruptedException e) {e.printStackTrace (); } ...}}}

Og nu skriver vi en anden klasse for at initialisere og starte vores tråd:

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

Vi skal ringe til Start() metode på tråde i NY tilstand (svarende til ikke startet). Ellers kaster Java en forekomst af IllegalThreadStateException undtagelse.

Lad os antage, at vi skal starte flere tråde:

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

Vores kode ser stadig ret simpel ud og ligner meget eksemplerne vi kan finde online.

Selvfølgelig, dette er langt fra produktionsklar kode, hvor det er af afgørende betydning at administrere ressourcer på den rigtige måde for at undgå for meget kontekstskift eller for meget hukommelsesforbrug.

Så for at blive produktionsklar skal vi nu skrive yderligere kedelplade at håndtere:

  • den konsekvente oprettelse af nye tråde
  • antallet af samtidige live-tråde
  • threads deallocation: meget vigtigt for daemon-tråde for at undgå lækager

Hvis vi vil, kan vi skrive vores egen kode til alle disse case-scenarier og endda nogle flere, men hvorfor skal vi genopfinde hjulet?

3. Den ExecutorService Ramme

Det ExecutorService implementerer Thread Pool-designmønsteret (også kaldet en replikeret arbejdstager- eller arbejder-besætningsmodel) og tager sig af trådstyringen, som vi nævnte ovenfor, plus det tilføjer nogle meget nyttige funktioner som trådgenanvendelighed og opgavekøer.

Især trådgenanvendelighed er meget vigtigt: i en storstilet applikation skaber tildeling og deallokering af mange trådobjekter en betydelig hukommelsesadministrationsomkostning.

Med arbejdertråde minimerer vi omkostningerne forårsaget af oprettelse af tråde.

For at lette poolkonfigurationen, ExecutorService leveres med en nem konstruktør og nogle tilpasningsindstillinger, såsom køtype, minimum og maksimum antal tråde og deres navngivningskonvention.

For flere detaljer om ExecutorService, læs venligst vores guide til Java ExecutorService.

4. Start af en opgave med eksekutører

Takket være denne stærke ramme kan vi skifte vores tankegang fra starttråde til at indsende opgaver.

Lad os se på, hvordan vi kan sende en asynkron opgave til vores eksekutor:

ExecutorService eksekutor = Executors.newFixedThreadPool (10); ... executor.submit (() -> {new Task ();});

Der er to metoder, vi kan bruge: udføre, der ikke returnerer noget, og Indsend, som returnerer en Fremtid indkapsling af beregningens resultat.

For mere information om Futures, læs venligst vores guide til java.util.concurrent.Future.

5. Starte en opgave med CompletableFutures

For at hente det endelige resultat fra a Fremtid objekt, vi kan bruge metode, der er tilgængelig i objektet, men dette vil blokere overordnet tråd indtil slutningen af ​​beregningen.

Alternativt kunne vi undgå blokken ved at tilføje mere logik til vores opgave, men vi er nødt til at øge kompleksiteten af ​​vores kode.

Java 1.8 introducerede en ny ramme oven på Fremtid konstruere for bedre at arbejde med beregningens resultat: Fuldført.

Fuldført redskaber CompletableStage, som tilføjer et stort udvalg af metoder til at vedhæfte tilbagekald og undgå al den VVS, der er nødvendig for at køre operationer på resultatet, når det er klar.

Implementeringen til at indsende en opgave er meget enklere:

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

supplyAsync tager en Leverandør indeholdende den kode, vi vil udføre asynkront - i vores tilfælde lambda-parameteren.

Opgaven sendes nu implicit til ForkJoinPool.commonPool (), eller vi kan specificere Eksekutor vi foretrækker som en anden parameter.

At vide mere om Fuldførende fremtid Læs vores vejledning til CompletableFuture.

6. Kørsel af forsinkede eller periodiske opgaver

Når vi arbejder med komplekse webapplikationer, kan det være nødvendigt at køre opgaver på bestemte tidspunkter, måske regelmæssigt.

Java har få værktøjer, der kan hjælpe os med at køre forsinkede eller tilbagevendende operationer:

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

6.1. Timer

Timer er en facilitet til at planlægge opgaver til fremtidig udførelse i en baggrundstråd.

Opgaver kan planlægges til engangsudførelse eller til gentagen udførelse med regelmæssige intervaller.

Lad os se, hvordan koden ser ud, hvis vi vil køre en opgave efter et sekund forsinkelse:

TimerTask-opgave = ny TimerTask () {offentlig ugyldig kørsel () {System.out.println ("Opgave udført på:" + ny dato () + "n" + "Trådens navn:" + Thread.currentThread (). GetName ( )); }}; Timer timer = ny Timer ("Timer"); lang forsinkelse = 1000L; timer.schedule (opgave, forsinkelse);

Lad os nu tilføje en tilbagevendende tidsplan:

timer.scheduleAtFixedRate (gentaget opgave, forsinkelse, periode);

Denne gang kører opgaven efter den angivne forsinkelse, og den gentager sig efter den tidsperiode, der er gået.

For mere information, se venligst vores guide til Java Timer.

6.2. ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor har metoder svarende til Timer klasse:

ScheduledExecutorService executorService = Executors.newScheduledThreadPool (2); ScheduledFuture resultFuture = executorService.schedule (callableTask, 1, TimeUnit.SECONDS);

For at afslutte vores eksempel bruger vi scheduleAtFixedRate () til tilbagevendende opgaver:

ScheduledFuture resultFuture = executorService.scheduleAtFixedRate (runnableTask, 100, 450, TimeUnit.MILLISECONDS);

Koden ovenfor udfører en opgave efter en indledende forsinkelse på 100 millisekunder, og derefter udfører den den samme opgave hver 450 millisekunder.

Hvis processoren ikke kan afslutte behandlingen af ​​opgaven i tide inden den næste forekomst, vises ScheduledExecutorService venter, indtil den aktuelle opgave er afsluttet, inden den næste starter.

For at undgå denne ventetid kan vi bruge scheduleWithFixedDelay (), som, som beskrevet af dens navn, garanterer en fast længde forsinkelse mellem gentagelser af opgaven.

For flere detaljer om PlanlagtExecutorService, læs venligst vores guide til Java ExecutorService.

6.3. Hvilket værktøj er bedre?

Hvis vi kører eksemplerne ovenfor, ser beregningens resultat det samme ud.

Så, hvordan vælger vi det rigtige værktøj?

Når en ramme tilbyder flere valg, er det vigtigt at forstå den underliggende teknologi for at tage en informeret beslutning.

Lad os prøve at dykke lidt dybere under hætten.

Timer:

  • tilbyder ikke realtidsgarantier: den planlægger opgaver ved hjælp af Object.wait (lang) metode
  • der er en enkelt baggrundstråd, så opgaver kører sekventielt, og en langvarig opgave kan forsinke andre
  • runtime undtagelser kastet i en TimerTask ville dræbe den eneste tilgængelige tråd og dermed dræbe Timer

ScheduledThreadPoolExecutor:

  • kan konfigureres med et hvilket som helst antal tråde
  • kan drage fordel af alle tilgængelige CPU-kerner
  • fanger undtagelser fra runtime og lader os håndtere dem, hvis vi vil (ved at tilsidesætte efterExecute metode fra ThreadPoolExecutor)
  • annullerer den opgave, der kastede undtagelsen, mens den lod andre fortsætte med at køre
  • er afhængig af OS-planlægningssystemet for at holde styr på tidszoner, forsinkelser, soltid osv.
  • leverer samarbejds-API, hvis vi har brug for koordinering mellem flere opgaver, som at vente på afslutningen af ​​alle indsendte opgaver
  • giver bedre API til styring af trådens livscyklus

Valget nu er indlysende, ikke?

7. Forskellen mellem Fremtid og Planlagt fremtid

I vores kodeeksempler kan vi observere det ScheduledThreadPoolExecutor returnerer en bestemt type Fremtid: Planlagt fremtid.

Planlagt fremtid udvider begge dele Fremtid og Forsinket grænseflader og arver således den yderligere metode getDelay der returnerer den resterende forsinkelse, der er knyttet til den aktuelle opgave. Det forlænges med RunnableScheduledFuture der tilføjer en metode til at kontrollere, om opgaven er periodisk.

ScheduledThreadPoolExecutor implementerer alle disse konstruktioner gennem den indre klasse ScheduledFutureTask og bruger dem til at kontrollere opgavens livscyklus.

8. Konklusioner

I denne vejledning eksperimenterede vi med de forskellige rammer, der var tilgængelige for at starte tråde og køre opgaver parallelt.

Derefter gik vi dybere ind i forskellene mellem Timer og ScheduledThreadPoolExecutor.

Kildekoden til artiklen er tilgængelig på GitHub.