Hvordan starte en tråd i Java

1. Introduksjon

I denne opplæringen skal vi utforske forskjellige måter å starte en tråd og utføre parallelle oppgaver.

Dette er veldig nyttig, spesielt når du arbeider med lange eller tilbakevendende operasjoner som ikke kan kjøres på hovedtråden, eller der brukergrensesnittinteraksjonen ikke kan settes på vent mens du venter på operasjonens resultater.

For å lære mer om detaljene i tråder, kan du definitivt lese veiledningen vår om livssyklusen til en tråd i Java.

2. Grunnleggende om å kjøre en tråd

Vi kan enkelt skrive litt logikk som går i en parallell tråd ved å bruke Tråd rammeverk.

La oss prøve et grunnleggende eksempel ved å utvide Tråd klasse:

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

Og nå skriver vi en andre klasse for å initialisere og starte tråden vår:

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

Vi burde ringe start() metoden på tråder i NY tilstand (tilsvarer ikke startet). Ellers vil Java kaste en forekomst av IllegalThreadStateException unntak.

La oss anta at vi må starte flere tråder:

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 (); }}

Koden vår ser fremdeles ganske enkel ut og veldig lik eksemplene vi kan finne på nettet.

Selvfølgelig, dette er langt fra produksjonsklar kode, hvor det er avgjørende å administrere ressurser på riktig måte, for å unngå for mye kontekstbytte eller for mye minnebruk.

Så for å bli produksjonsklar må vi nå skrive ekstra kjeleplate å håndtere:

  • den jevne opprettelsen av nye tråder
  • antall samtidige live-tråder
  • trådene deallocation: veldig viktig for daemon tråder for å unngå lekkasjer

Hvis vi vil, kan vi skrive vår egen kode for alle disse saksscenariene og til og med noen flere, men hvorfor skal vi gjenoppfinne hjulet?

3. Den ExecutorService Rammeverk

De ExecutorService implementerer Thread Pool-designmønsteret (også kalt en replikert arbeider eller arbeider-mannskapsmodell) og tar seg av trådadministrasjonen vi nevnte ovenfor, pluss at den legger til noen veldig nyttige funksjoner som gjenbrukbarhet og oppgavekøer.

Spesielt gjenbrukbarhet av tråder er veldig viktig: i en storstilt applikasjon skaper tildeling og distribusjon av mange trådobjekter en betydelig overhead for minnestyring.

Med arbeidertråder minimerer vi overhead forårsaket av trådoppretting.

For å lette bassengkonfigurasjonen, ExecutorService leveres med en enkel konstruktør og noen tilpasningsalternativer, for eksempel køtype, minimum og maksimalt antall tråder og deres navnekonvensjon.

For mer informasjon om ExecutorService, vennligst les vår guide til Java ExecutorService.

4. Starte en oppgave med utførere

Takket være dette kraftige rammeverket kan vi bytte tankesett fra starttråder til innsendingsoppgaver.

La oss se på hvordan vi kan sende en asynkron oppgave til vår eksekutor:

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

Det er to metoder vi kan bruke: henrette, som ikke gir noe tilbake, og sende inn, som returnerer a Framtid innkapslet beregningens resultat.

For mer informasjon om Fremtid, vennligst les vår guide til java.util.concurrent.Future.

5. Starte en oppgave med CompletableFutures

Å hente det endelige resultatet fra a Framtid objektet vi kan bruke metoden som er tilgjengelig i objektet, men dette vil blokkere overordnet tråd til slutten av beregningen.

Alternativt kan vi unngå blokken ved å legge til mer logikk i oppgaven vår, men vi må øke kodenes kompleksitet.

Java 1.8 introduserte et nytt rammeverk på toppen av Framtid konstruere for bedre å jobbe med beregningens resultat: Fullførbar fremtid.

Fullførbar fremtid redskaper CompletableStage, som legger til et stort utvalg av metoder for å feste tilbakeringinger og unngå alle rørleggerarbeidene som trengs for å kjøre operasjoner på resultatet etter at det er klart.

Implementeringen for å sende inn en oppgave er mye enklere:

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

supplyAsync tar en Leverandør inneholder koden vi vil utføre asynkront - i vårt tilfelle lambda-parameteren.

Oppgaven er nå implisitt sendt til ForkJoinPool.commonPool (), eller vi kan spesifisere Leder vi foretrekker som en andre parameter.

Å vite mer om Fullførbar fremtid Vennligst les vår guide til CompletableFuture.

6. Kjører forsinkede eller periodiske oppgaver

Når vi jobber med komplekse webapplikasjoner, kan det hende vi må kjøre oppgaver til bestemte tidspunkter, kanskje regelmessig.

Java har få verktøy som kan hjelpe oss med å utføre forsinkede eller gjentatte operasjoner:

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

6.1. Timer

Timer er et anlegg for å planlegge oppgaver for fremtidig kjøring i en bakgrunnstråd.

Oppgaver kan planlegges for engangsutførelse, eller for gjentatt utførelse med jevne mellomrom.

La oss se hvordan koden ser ut hvis vi vil kjøre en oppgave etter ett sekund forsinkelse:

TimerTask-oppgave = ny TimerTask () {public void run () {System.out.println ("Oppgave utført på:" + ny dato () + "n" + "Trådens navn:" + Thread.currentThread (). GetName ( )); }}; Timer timer = new Timer ("Timer"); lang forsinkelse = 1000L; timer.plan (oppgave, forsinkelse);

La oss nå legge til en gjenganger:

timer.scheduleAtFixedRate (gjentatt oppgave, forsinkelse, periode);

Denne gangen kjører oppgaven etter den angitte forsinkelsen, og den vil gjenta seg etter tidsperioden.

For mer informasjon, vennligst les vår guide til Java Timer.

6.2. ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor har metoder som ligner på Timer klasse:

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

For å avslutte eksemplet vårt bruker vi planAtFixedRate () for gjentatte oppgaver:

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

Koden ovenfor vil utføre en oppgave etter en innledende forsinkelse på 100 millisekunder, og etter det vil den utføre den samme oppgaven hver 450 millisekund.

Hvis prosessoren ikke kan fullføre behandlingen av oppgaven i tide før neste forekomst, vil PlanlagtExecutorService vil vente til den gjeldende oppgaven er fullført, før den starter neste.

For å unngå denne ventetiden kan vi bruke scheduleWithFixedDelay (), som, som beskrevet av navnet, garanterer en fast lengdeforsinkelse mellom gjentakelser av oppgaven.

For mer informasjon om PlanlagtExecutorService, vennligst les vår guide til Java ExecutorService.

6.3. Hvilket verktøy er bedre?

Hvis vi kjører eksemplene ovenfor, ser beregningsresultatet det samme ut.

Så, hvordan velger vi riktig verktøy?

Når et rammeverk tilbyr flere valg, er det viktig å forstå den underliggende teknologien for å ta en informert beslutning.

La oss prøve å dykke litt dypere under panseret.

Timer:

  • tilbyr ikke sanntidsgarantier: den planlegger oppgaver ved hjelp av Objekt. Vent (lang) metode
  • det er en enkelt bakgrunnstråd, slik at oppgaver kjøres sekvensielt, og en langvarig oppgave kan forsinke andre
  • runtime unntak kastet i en TimerTask ville drepe den eneste tilgjengelige tråden, og dermed drepe Timer

ScheduledThreadPoolExecutor:

  • kan konfigureres med et hvilket som helst antall tråder
  • kan dra nytte av alle tilgjengelige CPU-kjerner
  • fanger unntak for kjøretid og lar oss håndtere dem hvis vi vil (ved å overstyre afterExecute metode fra ThreadPoolExecutor)
  • avbryter oppgaven som kastet unntaket, mens andre lot fortsette å løpe
  • er avhengig av OS-planleggingssystemet for å holde oversikt over tidssoner, forsinkelser, soltid osv.
  • gir API for samarbeid hvis vi trenger koordinering mellom flere oppgaver, som å vente på fullføringen av alle innsendte oppgaver
  • gir bedre API for styring av trådens livssyklus

Valget nå er åpenbart, ikke sant?

7. Forskjellen mellom Framtid og Planlagt fremtid

I kodeeksemplene kan vi observere det ScheduledThreadPoolExecutor returnerer en bestemt type Framtid: Planlagt fremtid.

Planlagt fremtid utvider begge deler Framtid og Forsinket grensesnitt, og arver dermed tilleggsmetoden getDelay som returnerer den gjenværende forsinkelsen som er knyttet til den gjeldende oppgaven. Den utvides med RunnableScheduledFuture som legger til en metode for å sjekke om oppgaven er periodisk.

ScheduledThreadPoolExecutor implementerer alle disse konstruksjonene gjennom den indre klassen ScheduledFutureTask og bruker dem til å kontrollere oppgavens livssyklus.

8. Konklusjoner

I denne opplæringen eksperimenterte vi med de forskjellige rammene som var tilgjengelige for å starte tråder og kjøre oppgaver parallelt.

Så gikk vi dypere inn i forskjellene mellom Timer og ScheduledThreadPoolExecutor.

Kildekoden for artikkelen er tilgjengelig på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found