I java kan du lage flertrådede applikasjoner. Tråd og kjørbar, hvilken bør du velge? Hva er multithreading

Å skrive parallell kode er ikke en lett oppgave, og det er enda vanskeligere å verifisere riktigheten. Til tross for at Java gir omfattende støtte for multithreading og synkronisering på språk- og API-nivå, viser det seg i realiteten at skriving av riktig multithreaded Java-kode avhenger av erfaringen og fliden til den enkelte programmereren. Nedenfor er et sett med tips som hjelper deg med å øke den flertrådede Java-koden. Noen av dere er kanskje allerede kjent med disse tipsene, men det skader aldri å pusse opp dem en gang hvert par år.

Mange av disse tipsene kom fra læring og praktisk programmering og også etter å ha lest bøkene "Java concurrency in practice" og "Effective Java". Jeg anbefaler enhver Java-programmerer å lese den første to ganger; ja, det stemmer, TO GANGER. Samtidighet er et forvirrende og vanskelig tema å forstå (som for eksempel for noen - rekursjon), og etter å ha lest en gang, forstår du kanskje ikke alt helt.

Det eneste formålet med å bruke samtidighet er å lage skalerbare og raske applikasjoner, men det bør alltid huskes at hastighet ikke skal være et hinder for korrekthet. Java-programmet ditt må tilfredsstille sin invariant uansett om det startes i enkelt- eller flertråds form. Hvis du er ny på parallell programmering, for å komme i gang, sjekk ut ulike problemer oppstår kl parallell lansering programmer (for eksempel: dødlås, rasetilstand, ressurssult, etc.).

1. Bruk lokale variabler

Prøv alltid å bruke lokale variabler i stedet for klasse eller statiske felt. Noen ganger bruker utviklere klassefelt for å lagre minne og gjenbruke variabler, forutsatt at det å opprette en lokal variabel for hvert metodekall kan kreve et stort antall ekstra minne... Et eksempel på slik bruk er en samling, som er deklarert som et statisk felt og gjenbrukbar med clear ()-metoden. Dette setter klassen i en generell tilstand, som den ikke burde ha siden ble opprinnelig laget for parallell bruk på flere tråder. I koden nedenfor kalles execute ()-metoden fra forskjellige tråder, og en midlertidig samling var nødvendig for å implementere den nye funksjonaliteten. V original kode en statisk samling (List) ble brukt, og intensjonen til utvikleren var klar - å fjerne samlingen på slutten av execute ()-metoden slik at den kan gjenbrukes senere. Utvikleren mente koden hans var trådsikker fordi CopyOnWriteArrayList er trådsikker. Men dette er ikke tilfelle - execute ()-metoden kalles fra forskjellige tråder, og en av trådene kan få tilgang til dataene skrevet av den andre tråden til felles liste... Synkronisering levert av CopyOnWriteArrayList i i dette tilfellet utilstrekkelig til å sikre invariansen til execute ()-metoden.

Offentlig statisk klasse ConcurrentTask (privat statisk listetemp = Collections.synchronizedList (ny ArrayList ()); @Override public void execute (Meldingsmelding) (// Bruk den lokale midlertidige listen // List temp = new ArrayList (); // Legg til for å liste noe fra meldingen temp.add ("message.getId ()"); temp.add ("message.getCode ()"); temp.clear (); // kan nå gjenbrukes))

Problem: Dataene til en melding vil komme inn i en annen hvis to anrop for å utføre () "overlapper", dvs. den første tråden vil legge til ID-en fra den første meldingen, deretter vil den andre tråden legge til ID-en fra den andre meldingen (dette vil skje selv før listen er tømt), og dermed vil dataene til en av meldingene bli ødelagt.

Løsningsalternativer:

  1. Legg til en synkroniseringsblokk til den delen av koden der tråden legger til noe i den midlertidige listen og sletter den. Dermed vil ikke en annen tråd få tilgang til listen før den første er ferdig med den. I dette tilfellet vil denne delen av koden være entrådet, noe som vil redusere ytelsen til applikasjonen som helhet.
  2. Bruk en lokal liste i stedet for klassefeltet. Ja, det vil øke minnekostnadene, men det vil bli kvitt synkroniseringsblokken og gjøre koden mer lesbar. Du trenger heller ikke bekymre deg for midlertidige gjenstander – søppeloppsamleren tar seg av dem.

Bare ett av tilfellene presenteres her, men når jeg skriver parallell kode foretrekker jeg personlig lokal variable felt klasse, hvis sistnevnte ikke kreves av applikasjonsarkitekturen.

2. Foretrekk uforanderlige klasser fremfor mutbare

Den mest anerkjente praksisen innen multithreaded Java-programmering er bruken av uforanderlig (uforanderlig) klasser. Uforanderlige klasser som String, Integer og andre gjør det lettere å skrive parallell kode i Java, fordi du trenger ikke å bekymre deg for tilstanden til objektene i disse klassene. Uforanderlige klasser reduserer antallet synkroniseringselementer i koden din. Et objekt av en uforanderlig klasse, når det først er opprettet, kan ikke endres. Det beste eksemplet på en slik klasse er strengen (java.lang.String). Enhver operasjon for å modifisere en streng i Java (konvertering til store bokstaver, ta en understreng, etc.) vil resultere i opprettelsen av et nytt String-objekt for resultatet av operasjonen, og la den opprinnelige strengen være urørt.

3. Krympe synkroniseringsområdene

Enhver kode innenfor synkroniseringsdomenet kan ikke kjøres parallelt, og hvis 5 % av koden i programmet ditt er i synkroniseringsblokker, kan ytelsen til hele applikasjonen i henhold til Amdahls lov ikke forbedres mer enn 20 ganger. hovedårsaken Dette er fordi 5 % av koden alltid kjøres sekvensielt. Du kan redusere dette beløpet ved å krympe synkroniseringsområdene - prøv å bruke dem kun for kritiske seksjoner. Beste eksempel Synkroniseringsomfangsreduksjon - dobbeltsjekket låsing, som kan implementeres i Java 1.5 og høyere ved å bruke flyktige variabler.

4. Bruk en trådbasseng

Oppretting av strømmer (Tråd)- en kostbar operasjon. Hvis du vil bygge en skalerbar Java-applikasjon, må du bruke en trådpool. I tillegg til den tungvinte naturen til opprette-operasjonen, genererer manuell flytkontroll mye repeterende kode som, når den blandes med forretningslogikk, reduserer den generelle lesbarheten til koden. Flytkontroll er et rammeverks jobb, enten det er et Java-verktøy eller hva du måtte ønske å bruke. JDK har et godt organisert, rikt og fullt testet rammeverk kjent som Executor-rammeverket som kan brukes der det er behov for en trådpool.

5. Bruk synkroniseringsverktøy i stedet for å vente () og gi beskjed ()

Java 1.5 introduserte mange synkroniseringsverktøy som CyclicBarrier, CountDownLatch og Semaphore. Du bør alltid først lære hva JDK har for synkronisering før du bruker vent () og varsle (). Det vil være mye enklere å implementere leser-skriver-mønsteret med BlockingQueue enn med vente () og varsle (). Det vil også være mye lettere å vente på 5 tråder for å fullføre beregningen ved å bruke CountDownLatch enn å implementere det samme via vente () og varsle (). Sjekk ut java.util.concurrent-pakken for å skrive samtidig kode til Java den beste vei.

6. Bruk BlockingQueue for å implementere Produsent-Forbruker

Dette rådet følger av det forrige, men jeg har fremhevet det separat på grunn av dets betydning for parallelle applikasjoner brukt i den virkelige verden. Løsningen på mange multithreading-problemer er basert på Produsent-Consumer-mønsteret, og BlockingQueue - Den beste måten implementere det i Java. I motsetning til Exchanger, som kan brukes i tilfelle av en enkelt forfatter og leser, kan BlockingQueue brukes til å riktig behandling flere forfattere og lesere.

7. Bruk trådsikre samlinger i stedet for å blokkere samlinger

Trådsikre samlinger gir større skalerbarhet og ytelse enn deres blokkerende motparter (Collections.synchronizedCollection, etc.). СoncurrentHashMap, som etter min mening er den mest populære trådsikre samlingen, viser bedre ytelse enn å blokkere HashMap eller Hashtable når antallet lesere overstiger antallet forfattere. En annen fordel med trådsikre samlinger er at de er implementert med en ny låsemekanisme (java.util.concurrent.locks.Lock) og bruker de native synkroniseringsmekanismene levert av den underliggende maskinvaren og JVM. Bruk i tillegg CopyOnWriteArrayList i stedet for Collections.synchronizedList hvis du leser fra listen oftere enn å endre den.

8. Bruk semaforer for å lage begrensninger

For å lage et pålitelig og stabilt system, må du ha ressursbegrensninger (databaser, filsystem, stikkontakter osv.). Koden din skal aldri opprette og/eller bruke en uendelig mengde ressurser. Semaforer (java.util.concurrent.Semaphore) - et godt valgå opprette restriksjoner på bruken av dyre ressurser, for eksempel databasetilkoblinger (forresten, i dette tilfellet kan du bruke en tilkoblingspool). Semaforer vil bidra til å skape begrensninger og blokkere tråder hvis en ressurs er utilgjengelig.

9. Bruk synkroniseringsblokker i stedet for blokkeringsmetoder

Dette tipset utvides på spissen for å redusere synkroniseringsområder. Å bruke synkroniseringsblokker er en metode for å krympe synkroniseringsomfanget, som også lar deg låse på et annet objekt enn det som for øyeblikket er representert av denne pekeren. Den første kandidaten bør være en atomvariabel, deretter en flyktig variabel hvis de tilfredsstiller synkroniseringskravene dine. Hvis du trenger gjensidig ekskludering, bruk først ReentrantLock, eller en synkronisert blokk. Hvis du er ny til samtidig programmering og ikke utvikler noe vital viktig søknad, kan du ganske enkelt bruke en synkronisert blokk, som er tryggere og enklere.

10. Unngå å bruke statiske variabler

Som vist i det første tipset, kan statiske variabler, når de brukes i parallell kode, føre til mange problemer. Hvis du bruker en statisk variabel, sørg for at den er en konstant eller en uforanderlig samling. Hvis du tenker på å gjenbruke samlingen for å spare minne, gå tilbake til det første tipset.

11. Bruk Lås i stedet for synkronisert

Dette siste bonustipset bør brukes med forsiktighet. Lås grensesnitt - kraftig verktøy men hans styrke innebærer også et stort ansvar. Ulike gjenstander Låser på lese- og skriveoperasjoner lar deg implementere skalerbare datastrukturer som ConcurrentHashMap, men krever stor forsiktighet i programmeringen. I motsetning til en synkronisert blokk, frigjør ikke en tråd automatisk låsen. Du må eksplisitt ringe unlock () for å låse opp. Det er god praksis å kalle denne metoden i en endelig blokk slik at blokken slutter under alle forhold:

Lås.lås (); prøv (// gjør noe ...) til slutt (lock.unlock ();)

Konklusjon

Du ble presentert med tips for å skrive flertrådskode i Java. Nok en gang skader det aldri å lese «Java concurrency in practice» og «Effective Java» fra tid til annen. Du kan også utvikle den tankegangen du trenger for parallell programmering bare ved å lese andres kode og prøve å visualisere problemer under utviklingen. Spør deg selv til slutt hvilke regler du følger når du utvikler flertrådede Java-applikasjoner?

I teoretiske arbeider om multithreading kan du finne beskrivelser av tre oppgaver, som ifølge forfatterne dekker alle mulige multithreading-oppgaver - produsent-forbruker-oppgaven, leser-skribent-oppgaven, spisefilosof-oppgaven. Allegorien er vakker og ganske bra på sin egen måte, men etter min mening, for en umoden ung programmerer, sier den ingenting i det hele tatt. Derfor vil jeg beskrive problemene med klokketårnet mitt. Det er bare to problemer.

Det første problemet er tilgang til én ressurs fra flere tråder. Vi har allerede beskrevet problemet med en spade. Du kan utvide muligheten - det er en tank med vann (med en kran), 25 tørste gruvearbeidere og 5 krus for alle. Vi må forhandle, ellers kan drapet begynne. Dessuten er det nødvendig ikke bare å holde krusene intakte - det er også nødvendig å organisere alt slik at alle kan drikke. Dette beveger seg delvis til problem nummer to.
Det andre problemet er synkronisering av interaksjon. På en eller annen måte ble jeg tilbudt en oppgave - å skrive enkelt programå ha to bekker til å spille ping pong. Den ene skriver «Ping» og den andre skriver «Pong». Men de må gjøre det etter tur. La oss nå forestille oss at vi må gjøre den samme oppgaven, men for 4 tråder - vi spiller et par for et par.

De. problemformuleringen er veldig enkel. En gang - du må organisere ryddig og sikker tilgang til den delte ressursen. To - du må utføre trådene i en eller annen rekkefølge.
Det er opp til gjennomføringen. Og her venter mange vanskeligheter på oss, som de snakker med et pust om (og kanskje ikke forgjeves). La oss starte med en delt ressurs.

Delt ressurs for flere tråder

Jeg foreslår å umiddelbart demonstrere problemet med et enkelt eksempel. Dens oppgave er å starte 200 klassetråder Mottråd... Hver strøm får en lenke til en enkelt objekt Disk... Under utførelse kaller tråden dette objektets metode øketeller tusen ganger. Metoden øker en variabel disk med 1. Etter å ha lansert 200 tråder, venter vi til de slutter (vi sovner bare i 1 sekund - dette er nok). Og til slutt skriver vi ut resultatet. Se på koden - etter min mening er alt ganske gjennomsiktig der:

<200; i++) { CounterThread ct = new CounterThread(counter); ct.start(); } Thread.sleep(1000); System.out.println("Counter:" + counter.getCounter()); } } class Counter { private long counter = 0L; public void increaseCounter() { counter++; } public long getCounter() { return counter; } } class CounterThread extends Thread { private Counter counter; public CounterThread(Counter counter) { this.counter = counter; } @Override public void run() { for(int i=0; i<1000; i++) { counter.increaseCounter(); } } }

offentlig klasse CounterTester

for (int i = 0; i< 200 ; i ++ ) {

ct. start ();

Tråd. søvn (1000);

klasse teller

privat lang teller = 0L;

offentlig ugyldig økningTeller () (

teller ++;

offentlig lang getCounter () (

retur teller;

private Counter teller;

dette. teller = teller;

@ Overstyring

public void run () (

for (int i = 0; i< 1000 ; i ++ ) {

Logisk sett burde vi få følgende resultat - 200 tråder med 1000 tillegg = 200 000. Men, skrekk, dette er slett ikke tilfelle. Resultatene mine varierer, men tydeligvis ikke 200 000. Hva er problemet? Problemet er at fra 200 tråder prøver vi samtidig å kalle metoden øketeller... Ved første øyekast skjer det ikke noe forferdelig i det - vi legger bare til variabelen disk enhet. Hva er så forferdelig med det?
Det forferdelige er at den tilsynelatende harmløse koden for å legge til en faktisk ikke kjøres i ett trinn. Først leser vi verdien av variabelen inn i et register, så legger vi en til den, så skriver vi resultatet tilbake til variabelen. Som du kan se, er det mer enn ett trinn (i hemmelighet - det er enda mer enn tre trinn som jeg beskrev). Og forestill deg nå at to tråder (eller enda flere) samtidig leser verdien av en variabel - for eksempel var det en verdi på 99. Nå legger begge trådene en til 99, får begge 100 og begge skriver denne verdien til variabelen. Hva skjer der? Det er lett å se at det blir 100. Og det burde være 101. Det kan være enda verre hvis en eller annen tråd "klarer" å telle 98 og "står seg fast" i køen av tråder for utførelse. Da får vi ikke 100 engang. Hitch 🙂

Delt ressurstilgang er et av de største problemene med multithreading. For hun er veldig utspekulert. Du kan gjøre alt veldig pålitelig, men da vil ytelsen synke. Og så snart du gir "slakk" (bevisst, for produktiviteten), vil det helt sikkert oppstå en situasjon at "slakken" kommer ut i all sin prakt.

Det magiske ordet - synkronisert

Hva kan gjøres for å bli kvitt situasjonen vi er i med våre fantastiske bekker. La oss starte med en liten spekulasjon. Når vi kommer til butikken går vi til kassen for å betale. Kassereren betjener kun én person om gangen. Vi stiller alle opp for henne. Faktisk blir kassaapparatet en eksklusiv ressurs som kun kan brukes av én kunde om gangen. Multithreading tilbyr nøyaktig samme måte - du kan definere en bestemt ressurs som eksklusiv for kun én tråd om gangen. En slik ressurs kalles en "monitor". Dette er det vanligste objektet som en tråd skal "fange". Alle tråder som ønsker tilgang til denne monitoren (objektet) står i kø. Og for dette trenger du ikke å skrive spesiell kode - du trenger bare å prøve å "fange" skjermen. Men hvordan definerer du dette? La oss finne ut av det.
Jeg foreslår å kjøre vårt eksempel, men med ett ekstra ord i metodebeskrivelsen øketeller- dette ordet synkronisert.

pakken edu.javacourse.counter; offentlig klasse CounterTester (public static void main (String args) kaster InterruptedException (Counter counter = new Counter (); for (int i = 0; i<200; i++) { CounterThread ct = new CounterThread(counter); ct.start(); } Thread.sleep(1000); System.out.println("Counter:" + counter.getCounter()); } } class Counter { private long counter = 0L; public synchronized void increaseCounter() { counter++; } public long getCounter() { return counter; } } class CounterThread extends Thread { private Counter counter; public CounterThread(Counter counter) { this.counter = counter; } @Override public void run() { for(int i=0; i<1000; i++) { counter.increaseCounter(); } } }

pakke edu. javakurs. disk;

offentlig klasse CounterTester

public static void main (String args) kaster InterruptedException (

Tellerteller = ny Teller ();

for (int i = 0; i< 200 ; i ++ ) {

CounterThread ct = ny CounterThread (teller);

ct. start ();

Tråd. søvn (1000);

System. ute. println ("Teller:" + teller. getCounter ());

klasse teller

privat lang teller = 0L;

offentlig synkronisert void økningTeller () (

teller ++;

offentlig lang getCounter () (

retur teller;

klasse CounterThread forlenger tråden

private Counter teller;

offentlig CounterThread (Tellerteller) (

dette. teller = teller;

@ Overstyring

public void run () (

for (int i = 0; i< 1000 ; i ++ ) {

disk. øketeller ();

Og ... se og se. Alt fungerte. Vi får det forventede resultatet - 200 000. Hva gjør dette magiske ordet - synkronisert ?
Ord synkronisert sier at før en tråd kan kalle denne metoden på objektet vårt, må den "fange" objektet vårt og deretter utføre den nødvendige metoden. Nok en gang og forsiktig (noen ganger foreslås en litt annen tilnærming, som etter min mening er ekstremt farlig og feilaktig - jeg skal beskrive det litt senere) - først "fanger" tråden (låser - fra ordet lås - lås, til blokk) monitorobjektet (i vårt tilfelle er det klasseobjekt Disk) og først etter det vil tråden kunne utføre metoden øketeller... Eksklusivt, helt alene uten konkurrenter.
Det er en annen tolkning synkronisert som kan være misvisende - det høres omtrent slik ut: flere tråder kan ikke gå inn i en synkronisert metode samtidig. Dette er ikke sant. For da viser det seg at hvis en klasse har flere metoder synkronisert, så kan to forskjellige metoder for samme objekt utføres samtidig, merket som synkronisert... Dette er ikke sant. Hvis en klasse har 2, 3 eller flere metoder synkronisert, så når minst én er utført, låses hele objektet. Dette betyr at alle metoder utpekt som synkronisert er ikke tilgjengelig for andre strømmer. Hvis metoden ikke er merket som sådan. det er ikke et problem - gjør det for helsen din.
Og nok en gang – først «fanget» de, så utførte de metoden, så «slapp de». Nå er objektet fritt, og den som var den første til å fange det fra bekkene har rett.
I tilfelle metoden er erklært som statisk, så blir hele klassen overvåkingsobjektet og tilgang til den blokkeres på nivået til alle objekter i denne klassen.

Da de diskuterte artikkelen, påpekte de for meg feilen, noe jeg bevisst innrømmet (for enkelhets skyld), men det er nok fornuftig å nevne det. Det handler om metoden getCounter... Strengt tatt bør den også betegnes som synkronisert fordi i det øyeblikket variabelen vår endres, vil en annen tråd ønske å lese den. Og for å unngå problemer, må tilgang til denne variabelen gjøres synkronisert i alle metoder.
Skjønt som for getCounter, så her kan du bruke en enda mer interessant funksjon - atomiteten til operasjoner. Du kan lese om det i artikkelen Atomic access. Hovedideen er at lesing og skriving av enkelte elementære typer og referanser gjøres i ett trinn og er i prinsippet trygt. Hvis feltet disk var for eksempel int, da ville det være mulig å lese i en ikke-synkron metode. For type lang og dobbelt vi må deklarere en variabel disk hvordan flyktige... Hvorfor dette kan være rart - det bør tas i betraktning int består av 4 byte og du kan forestille deg situasjonen at tallet ikke vil bli skrevet i ett trinn. Men dette er rent teoretisk - JVM garanterer oss at lesing og skriving av en elementær int-type gjøres i ett trinn og ingen tråd vil kunne kile seg inn i denne operasjonen og ødelegge noe.

Det er en annen måte å bruke ordet på synkronisert- ikke i beskrivelsen av metoden, men inne i koden. La oss endre eksemplet nok en gang i metodedelen øketeller.

pakken edu.javacourse.counter; offentlig klasse CounterTester (public static void main (String args) kaster InterruptedException (Counter counter = new Counter (); for (int i = 0; i<200; i++) { CounterThread ct = new CounterThread(counter); ct.start(); } Thread.sleep(1000); System.out.println("Counter:" + counter.getCounter()); } } class Counter { private long counter = 0L; public void increaseCounter() { synchronized(this) { counter++; } } public long getCounter() { return counter; } } class CounterThread extends Thread { private Counter counter; public CounterThread(Counter counter) { this.counter = counter; } @Override public void run() { for(int i=0; i<1000; i++) { counter.increaseCounter(); } } }

pakke edu. javakurs. disk;

offentlig klasse CounterTester

public static void main (String args) kaster InterruptedException (

Tellerteller = ny Teller ();

for (int i = 0; i< 200 ; i ++ ) {

Før vi lærer om Java-strømmer, la oss se inn i nær fremtid. Tenk deg at du har søkt om CV og har blitt intervjuet. Du og et par dusin fremtidige kolleger ble invitert til å jobbe i et stort programvareselskap. Blant andre problemer, må du sende inn papirdokumenter for ansettelse til en sliten HR-ansatt.

For å få fart på prosessen kan søkere til stillingen deles i to grupper og fordeles mellom to HR-ledere (hvis det er noen i bedriften). Som et resultat får vi akselerasjonen av prosessen på grunn av parallellen ( parallell) arbeid med registrering.

Hvis det bare er én personelloffiser i bedriften, så må man på en eller annen måte komme seg ut. For eksempel kan du igjen dele alle inn i to grupper, for eksempel intervjue jenter og gutter vekselvis.

Eller etter et annet prinsipp: siden det er flere i den nedre gruppen, vil vi alternere to jenter for en gutt.

Denne måten å organisere arbeidet på kalles flertrådet... Vår slitne HR-ansvarlig bytter til forskjellige grupper for å gjøre en annen ansatt ut av dem. Det kan være elleve grupper og fire personelloffiserer. I dette tilfellet, multithreaded ( flertråding) Behandling vil foregå parallelt av flere HR-er, som kan ta neste person fra en hvilken som helst gruppe for å behandle dokumentene hans.

Prosesser

Prosessen ( prosess) i dette tilfellet vil det være organisering av arbeidet med å motta dokumenter. Det kan skilles mellom flere prosesser i en organisasjon: regnskap, programvareutvikling, møter med kunder, lagerdrift, etc. Ressurser tildeles for hver prosess: lokaler, ansatte for gjennomføringen. Prosesser er isolert fra hverandre: HR-personell har ikke tilgang til regnskapsdatabasen, og kontoansvarlige løper ikke rundt på lageret. Hvis en prosess trenger å få tilgang til andres ressurser, er det nødvendig å etablere interprosesskommunikasjon: notater, fellesmøter.

Strømmer

Arbeidet i prosessen er organisert i form av tråder (java-tråd). For HR-avdelingen er flyt organisering av arbeidet for vedlikehold av konsernet. Det aller første bildet viser én bekk, de neste tre – to. Innenfor prosessen kan tråder gjennomføres parallelt – to HR-offiserer tar på seg to eller flere grupper av fremtidige ansatte. Samspillet mellom HR-offiserer med grupper - prosesseringsflyter i prosessen - kalles synkroniserte strømmer... I tegningene av designet av en personelloffiser i to grupper, vises metoder: uniform (jente - gutt - jente - gutt) og med forskjellige prioriteringer (to jenter veksler med en gutt). Tråder har tilgang til ressursene i prosessen som de tilhører: grupper til HR-ansvarlig får prøver av søknadsskjemaer, penner for utfylling av dokumenter. Men hvis trådene samhandler med ting som er felles for dem, er hendelser mulige. Hvis en personelloffiser ber om å få rope navnet på den siste personen i køen, så er han ved to grupper ikke sikker på på forhånd om han vil høre et kvinnenavn eller et mannsnavn. Slike datatilgangskonflikter, låser og hvordan de løses er et svært viktig tema.

Strømtilstander

Hver tråd er i en av følgende tilstander (tilstand):
  • Opprettet (Ny) - køen til personaloffiseren forberedes, folk organiseres.
  • Kjørbar - køen vår har stilt seg opp til HR-ansvarlig og er under behandling.
  • Blokkert - den unge mannen sist i køen prøver å rope ut et navn, men da han hørte at jenta i nabogruppen begynte å gjøre det før ham, ble han stille.
  • Avsluttet - hele køen ble fullført av personaloffiseren og det er ikke behov for det.
  • Venter - en kø venter på signal fra en annen.
Organiseringen av strømmer og deres samhandling er grunnlaget for effektiv drift av prosesser.

Tilbake til IT-verdenen

I det 21. århundre har multithreading og parallell utførelse blitt aktuelt. Siden 90-tallet av forrige århundre har multitasking-operativsystemer Windows, macOS og Linux blitt godt etablert på hjemmedatamaskiner. De inneholder ofte fire eller flere kjerneprosessorer. Antall parallelle blokker med GPU-skjermkort har allerede oversteget tusen. Populære programmer er skrevet under hensyntagen til multithreading, for eksempel moderne versjoner av programvare for behandling av grafikk, video eller drift med store datamengder: Adobe Photoshop, WinRar, Mathematica, moderne spill. Java multithreading er et veldig viktig, etterspurt og komplekst emne. Derfor er det mange oppgaver i CodeGym-kurset for å håndtere det veldig bra. Java-eksempler på multithreading vil hjelpe deg med å mestre de grunnleggende nyansene og finessene i dette området, trådsynkronisering.

Prosess

Prosess(prosess) er en kjørende forekomst av et program som operativsystemet (OS) har allokert minne, prosessortid/kjerner og andre ressurser til. Det er viktig at minnet tildeles separat, adresseområdene til forskjellige prosesser er utilgjengelige for hverandre. Hvis prosesser trenger å kommunisere, kan de gjøre det ved å bruke filer, rør og andre kommunikasjonsmetoder mellom prosesser.

Strømme

Java-tråd (tråd). Noen ganger, for å unngå forvirring med andre Java-Stream-klasser og lignende, blir Java-strømmer ofte oversatt som en tråd. De bruker ressursene som er allokert til prosessen og er måten prosessen utføres på. Hovedtråden utfører hovedmetoden og avslutter. Under utførelsen av prosessen kan ytterligere tråder (underordnet) dannes. Tråder i én prosess kan utveksle data med hverandre. Java multithreading krever at datasynkronisering tas i betraktning, ikke glem det. I Java avsluttes en prosess når den siste tråden er ferdig kjørt. For bakgrunnsoppgaver kan tråden startes som en demon, som skiller seg fra den vanlige ved at de vil bli tvangsavbrutt når alle ikke-demonstråder i prosessen avsluttes.

Første flertrådsapplikasjon

Det er over et halvt dusin måter å lage strømmer på, og vi vil dekke dem i detalj i CodeGym-kurset. Først, la oss bli kjent med en av de grunnleggende. Det er en spesiell klasse Thread in the run () metode som du må skrive kode som implementerer logikken til programmet. Etter å ha opprettet en tråd, kan du starte den ved å kalle start ()-metoden. La oss skrive et demoprogram som implementerer et eksempel på Java multithreading. klasse PeopleQueue utvider tråden ( // Vår medarbeiderkø, arvet fra Trådklassen private strengnavn; PeopleQueue (String... Names) ( // Konstruktør, argument er en rekke ansattes navn dette. navn = navn; ) @Overstyr offentlig ugyldig kjøring () ( // Denne metoden kalles når tråden starter for (int i = 0; i< names. length; i++ ) { // Utgang i sløyfe med en pause på 0,5 sekunder for neste ansatt System. ute. println ( "Dokumenter behandlet:"+ navn [i]); prøve (søvn (500); // Forsinkelse 0,5 sek) fangst (Unntak e) ()))) offentlig klasse HR ( // Klasse for å demonstrere hvordan tråden fungerer offentlig statisk tomrom hoved (String args) ( // Lag to køer PeopleQueue queue1 = ny PeopleQueue ("Ivan", "Sergey", "Nikolay", "Ferdinand", "Vasily"); PeopleQueue queue2 = ny PeopleQueue ("Maria", "Lyudmila", "Alice", "Karina", "Olga"); System. ute. println ("Kom i gang!"); // Melding fra hovedtråden til programmet kø1. start (); // Start én kø (undertråd) kø2. start (); // Start den andre (undertråden))) La oss starte programmet. I konsollen kan du se meldingen som sendes ut av hovedtråden. Videre sender hver underordnet tråd queue1 og queue2 vekselvis ut meldinger til den felles konsollen for dem om den neste behandlede ansatt. Et av de mulige alternativene for programmet: Start! Behandlede dokumenter: Maria Behandlede dokumenter: Ivan Behandlede dokumenter: Lyudmila Behandlede dokumenter: Sergey Behandlede dokumenter: Alice Behandlede dokumenter: Nikolay Behandlede dokumenter: Karina Behandlede dokumenter: Ferdinand Behandlede dokumenter: Olga Behandlede dokumenter: Vasily Prosessen ferdig med utgangskode 0 Multithreading i Java– Temaet er vanskelig og mangefasettert. Å vite hvordan du skriver kode ved hjelp av parallell, multitasking og multithreaded databehandling vil hjelpe deg med å effektivt implementere oppgaver på moderne multi-core prosessorer og klynger av mange datamaskiner. bør bruke klassen java.lang.Thread. Denne klassen definerer alle metodene som trengs for å lage tråder, administrere deres tilstand og synkronisere dem.

Hvordan bruker jeg trådklassen?

Det er to muligheter.

  • Først kan du lage din egen barneklasse basert på trådklassen. Når du gjør dette, må du overstyre kjøremetoden. Implementeringen din av denne metoden vil kjøre på en egen tråd.
  • For det andre kan klassen din implementere Runnable-grensesnittet. I dette tilfellet, innenfor klassen din, må du definere en kjøremetode som vil fungere som en egen tråd.

Den andre metoden er spesielt praktisk i tilfeller der klassen din må arve fra en annen klasse (for eksempel fra Applet-klassen) og samtidig trenger du multithreading. Siden Java-programmeringsspråket ikke har multippel arv, er det ikke mulig å opprette en klasse som er foreldre for Applet- og Thread-klassene. I dette tilfellet er implementering av Runnable-grensesnittet den eneste måten å løse problemet på.

Tråd klassemetoder

Thread-klassen definerer tre felt, flere konstruktører og et stort antall metoder for å jobbe med tråder. Nedenfor har vi gitt en kort beskrivelse av felt, konstruktører og metoder.

Med konstruktører kan du lage strømmer på forskjellige måter, spesifisere et navn og en gruppe for dem, om nødvendig. Navnet er for strømidentifikasjon og er et valgfritt attributt. Når det gjelder gruppene, er de designet for å organisere beskyttelsen av tråder fra hverandre innenfor samme applikasjon.

Metodene til Thread-klassen gir alle nødvendige funksjoner for å administrere tråder, inkludert synkronisering av dem.

Enger

Tre statiske felt er for prioritering av tråder.

  • NORM_PRIORITY

Vanlig

offentlig endelig statisk int NORM_PRIORITY;
  • MAX_PRIORITY

Maksimum

offentlig endelig statisk int MAX_PRIORITY;
  • MIN_PRIORITY

Minimum

offentlig endelig statisk int MIN_PRIORITY;

Konstruktører

Opprette et nytt trådobjekt

offentlig tråd ();

Oppretting av et nytt trådobjekt som spesifiserer objektet som kjøremetoden skal kalles for

offentlig tråd (kjørbart mål); offentlig tråd (kjørbart mål, strengnavn);

Opprette et trådobjekt med navnet

offentlig tråd (strengnavn);

Opprett et nytt trådobjekt som spesifiserer trådgruppen og objektet som kjøremetoden kalles for

offentlig tråd (ThreadGroup group, Runnable target);

Ligner på den forrige, men spesifiserer i tillegg navnet på det nye trådobjektet

offentlig tråd (trådgruppegruppe, kjørbart mål, strengnavn);

Opprett et nytt trådobjekt som spesifiserer trådgruppen og objektnavnet

offentlig tråd (trådgruppegruppe, strengnavn);

Metoder

  • activeCount

Det gjeldende antallet aktive tråder i gruppen som tråden tilhører

offentlig statisk int activeCount ();
  • sjekk tilgang

Den gjeldende tråden har lov til å endre trådobjektet

offentlig void checkAccess ();
  • countStackFrames

Bestemme antall rammer på stabelen

public int countStackFrames ();
  • gjeldende tråd

Bestemme gjeldende løpende tråd

offentlig statisk tråd gjeldende tråd ();
  • ødelegge

Tving avslutning av en tråd

offentlig ugyldig ødelegge ();
  • dumpStack

Viser gjeldende innhold i stabelen for feilsøking

offentlig statisk tomrom dumpStack ();
  • oppregne

Henter alle trinnene i en gitt gruppe

offentlig statisk int enumerate (Thread tarray);
  • getName

Bestemme navnet på en bekk

offentlig endelig streng getName ();
  • fåPrioritet

Bestemme gjeldende prioritet til en tråd

offentlig endelig int getPriority ();
  • getThreadGroup

Bestemme gruppen som strømmen tilhører

offentlig endelig trådgruppe getThreadGroup ();
  • avbryte

Avbryte en strøm

offentlig ugyldig avbrudd ();
  • avbrutt
offentlig statisk boolsk avbrutt ();
  • er i live

Avgjør om en tråd kjører eller ikke

offentlig endelig boolesk isAlive ();
  • er Daemon

Avgjøre om en tråd er en demon

offentlig endelig boolsk isDaemon ();
  • er avbrutt

Avgjøre om en tråd blir avbrutt

offentlig boolsk er Avbrutt ();
  • bli med

Venter på at en tråd skal fullføres

offentlig endelig ugyldig delta ();

Venter på at en tråd skal fullføres i et spesifisert tidsrom. Tiden er satt i millisekunder.

offentlig endelig ugyldig sammenføyning (lang millimeter);

Venter på at en tråd skal fullføres i et spesifisert tidsrom. Tiden er satt i millisekunder og nanosekunder

offentlig endelig void join (lang millis, int nanos);
  • gjenoppta

Starter en midlertidig suspendert tråd

offentlig endelig ugyldig CV ();

Metoden kalles hvis tråden ble opprettet som et objekt med Runnable-grensesnittet

offentlig void run ();
  • settDaemon

Innstilling for en daemon-modus-tråd

offentlig endelig void setDaemon (boolsk på);
  • settnavn

Angi strømnavn

public final void setName (strengnavn);
  • settPrioritet

Angi trådprioritet

offentlig endelig void setPriority (int newPriority);
  • søvn
offentlig statisk tomromssøvn (lang millimeter);

Forsinkelse av flyten for tilbaketiden. Tiden er satt i millisekunder og nanosekunder

offentlig statisk void søvn (lang millis, int nanos);
  • start

Lanserer en tråd for utførelse

offentlig ugyldig start ();
  • Stoppe

Stopper trådkjøring

offentlig endelig ugyldig stopp ();

Unormal avslutning av trådutførelse med spesifisert unntak

offentlig endelig ugyldig stopp (kastbar obj);
  • utsette

Suspendere en strøm

offentlig endelig ugyldig suspendering ();
  • til String

En streng som representerer strømobjektet

offentlig streng til streng ();
  • utbytte

Suspenderer gjeldende tråd slik at kontrollen overføres til en annen tråd

offentlig statisk void yield ();

Opprette en barneklasse basert på trådklassen

La oss vurdere den første måten å implementere multithreading på, basert på arv fra Thread-klassen. Når du bruker denne metoden, definerer du en egen klasse for strømmen, for eksempel slik:

klasse DrawRectangles forlenger tråden (... public void run () (...))

Dette definerer DrawRectangles-klassen, som er et barn av Thread-klassen.

Vær oppmerksom på løpsmetoden. Når du oppretter klassen din basert på Thread-klassen, bør du alltid definere denne metoden, som vil bli utført i en egen tråd.

Merk at kjøremetoden ikke kalles direkte av noen annen metode. Den får kontroll når tråden starter med startmetoden.

Hvordan skjer dette?

La oss se på prosedyren for å starte en tråd ved å bruke noen DrawRectangles-klasse som eksempel.

Først må applikasjonen din opprette et objekt av Thread-klassen:

public class MultiTask2 utvider Applet (Thread m_DrawRectThread = null;... public void start () (if (m_DrawRectThread == null) (m_DrawRectThread = new DrawRectangles (this); m_DrawRectThread.start ();)))

Objektoppretting utføres av den nye operatøren i startmetoden, som tar kontroll når brukeren åpner et HTML-dokument med en applet. Umiddelbart etter opprettelsen startes tråden for utførelse, som startmetoden kalles for.

Når det gjelder kjøringsmetoden, hvis tråden brukes til å utføre periodisk arbeid, inneholder denne metoden en uendelig løkke inne i den. Når sløyfen avsluttes og kjøremetoden kommer tilbake, går tråden ut normalt, ikke unormalt. Du kan bruke avbruddsmetoden for å avslutte en tråd unormalt.

Stopping av en løpende tråd utføres ved hjelp av stoppmetoden. Vanligvis stoppes alle kjørende tråder opprettet av en applet ved hjelp av stoppmetoden til appletklassen:

offentlig ugyldig stopp () (hvis (m_DrawRectThread! = null) (m_DrawRectThread.stop (); m_DrawRectThread = null;))

Husk at denne metoden kalles når brukeren forlater webserversiden som inneholder appleten.

Implementering av det kjørbare grensesnittet

Den ovennevnte måten å lage tråder på som objekter i trådklassen eller klasser som er arvet fra den virker ganske naturlig. Denne metoden er imidlertid ikke den eneste. Hvis du bare trenger å lage én tråd som kjører samtidig med appletkoden, er det lettere å velge den andre metoden ved å bruke Runnable-grensesnittet.

Ideen er at hovedappletklassen, som er et barn av Applet-klassen, i tillegg implementerer Runnable-grensesnittet, som vist nedenfor:

offentlig klasse MultiTask utvider Applet implementerer Runnable (Thread m_MultiTask = null;... public void run () (...) public void start () (hvis (m_MultiTask == null) (m_MultiTask = new thread (this); m_MultiTask. start ();)) offentlig ugyldig stopp () (hvis (m_MultiTask! = null) (m_MultiTask.stop (); m_MultiTask = null;)))

Inne i klassen må du definere en kjøremetode som skal kjøres i en egen tråd. I dette tilfellet kan vi anta at koden til appleten og koden til kjøremetoden fungerer samtidig som forskjellige tråder.

Den nye operatøren brukes til å lage en strøm. En tråd opprettes som et objekt av Thread-klassen, og en referanse til appletklassen sendes til konstruktøren:

m_MultiTask = ny tråd (dette);

I dette tilfellet, når tråden starter, vil kontroll bli mottatt av kjøremetoden definert i appletklassen.

Hvordan starter jeg en tråd?

Starten utføres, som før, ved startmetoden. Vanligvis startes en tråd fra startmetoden til en applet når brukeren viser webserversiden som inneholder appleten. Stopp av flyten gjøres ved stoppmetoden.

Hallo! I denne artikkelen vil jeg gi deg en rask oversikt over prosesser, tråder og det grunnleggende om flertrådsprogrammering i Java-språket.
Den mest åpenbare bruken for multithreading er i grensesnittprogrammering. Multithreading er uunnværlig når du vil at GUI skal fortsette å svare på brukerinndata mens noe informasjonsbehandling pågår. For eksempel kan tråden som er ansvarlig for grensesnittet vente på fullføringen av en annen tråd som laster ned en fil fra Internett, og på dette tidspunktet vise en animasjon eller oppdatere fremdriftslinjen. I tillegg kan det stoppe tråden som laster filen hvis "avbryt"-knappen ble trykket.

En annen populær, og kanskje en av de mest hardcore, bruksområdene for multithreading er spilling. I spill kan ulike tråder stå for nettverk, animasjon, fysikkberegninger osv.

La oss begynne. Først om prosessene.

Prosesser

En prosess er en samling av kode og data som deler et felles virtuelt adresserom. Oftest består ett program av én prosess, men det finnes unntak (for eksempel oppretter Chrome-nettleseren en egen prosess for hver fane, noe som gir den noen fordeler, for eksempel uavhengighet av faner fra hverandre). Prosesser er isolert fra hverandre, så direkte tilgang til minnet om andres prosess er umulig (interaksjon mellom prosesser utføres ved hjelp av spesielle midler).

For hver prosess oppretter operativsystemet et såkalt "virtuelt adresserom" som prosessen har direkte tilgang til. Denne plassen tilhører prosessen, inneholder kun dens data og står til full disposisjon. Operativsystemet er ansvarlig for hvordan det virtuelle rommet til prosessen projiseres på det fysiske minnet.

Diagrammet for denne interaksjonen er vist på bildet. Operativsystemet opererer på såkalte minnesider, som ganske enkelt er et område med en viss fast størrelse. Hvis en prosess går tom for minne, tildeler systemet ytterligere sider fra det fysiske minnet. Virtuelle minnesider kan tilordnes fysisk minne i hvilken som helst rekkefølge.

Når programmet starter, oppretter operativsystemet en prosess, laster programkoden og dataene inn i adresseområdet, og starter deretter hovedtråden til den opprettede prosessen.

Strømmer

Én tråd er én enhet for kodeutførelse. Hver tråd utfører sekvensielt instruksjoner fra prosessen den tilhører, parallelt med de andre trådene i den prosessen.

Uttrykket «parallelt med andre bekker» bør diskuteres separat. Det er kjent at det er én utførelsesenhet per en prosessorkjerne i hvert øyeblikk. Det vil si at en enkeltkjerneprosessor bare kan behandle kommandoer sekvensielt, én om gangen (i et forenklet tilfelle). Å lansere flere parallelle tråder er imidlertid også mulig på systemer med enkeltkjerneprosessorer. I dette tilfellet vil systemet med jevne mellomrom bytte mellom tråder, vekselvis tillater en eller annen tråd å bli utført. Dette kalles pseudo-parallelisme. Systemet husker tilstanden (konteksten) til hver tråd før du bytter til en annen tråd, og gjenoppretter den når den går tilbake til utføringen av tråden. Trådkonteksten inkluderer parametere som stabelen, et sett med prosessorregisterverdier, adressen til kommandoen som utføres, og så videre ...

Enkelt sagt, i pseudo-parallell utførelse av tråder, skynder prosessoren mellom utføringen av flere tråder, og utfører på sin side en del av hver av dem.

Slik ser det ut:

De fargede firkantene i figuren er prosessorinstruksjoner (grønn - instruksjoner for hovedtråden, blå - for sidetråden). Utførelsen går fra venstre til høyre. Etter at sidestrømmen er startet, begynner dens instruksjoner å kjøre ispedd instruksjonene til hovedtråden. Antall instruksjoner som skal utføres for hver tilnærming er ikke definert.

Sammenblanding av parallelle trådinstruksjoner kan i noen tilfeller føre til datatilgangskonflikter. Den neste artikkelen vil bli viet til problemene med trådinteraksjon, men foreløpig om hvordan tråder startes i Java ...

Lansering av strømmer

Hver prosess har minst én løpende tråd. Tråden som kjøringen av programmet starter fra kalles hovedtråden. I Java, etter å ha opprettet en prosess, starter kjøringen av hovedtråden fra hovedmetoden (). Deretter, om nødvendig, på stedene spesifisert av programmereren, og når betingelsene spesifisert av ham er oppfylt, lanseres andre sidestrømmer.

I Java er en tråd representert som en etterkommer av Thread-klassen. Denne klassen innkapsler standard gjengemekanismer.

Det er to måter å starte en ny tråd på:

Metode 1
Lag et objekt av Thread-klassen ved å sende noe til det i konstruktøren som implementerer Runnable-grensesnittet. Dette grensesnittet inneholder en run()-metode som vil kjøre på en ny tråd. Tråden vil fullføres når kjøremetoden () er ferdig.

Det ser slik ut:

Class SomeThing // Noe som implementerer Runnable-grensesnittet implementerer Runnable // (som inneholder en kjøring ()-metode) (public void run () // Denne metoden vil kjøre på en sidestrøm (System.out.println ("Hei fra en side) stream!") ;)) public class Program // En klasse med en hoved()-metode (statisk SomeThing mThing; // mThing er et objekt av en klasse som implementerer Runnable-grensesnittet public static void main (String args) (mThing = new SomeThing (); Thread myThready = new Thread (mThing); // Opprett tråd "myThready" myThready.start (); // Start tråd System.out.println ("Hovedtråd ferdig ...");))

For å forkorte koden ytterligere kan du sende et objekt av en ikke navngitt indre klasse som implementerer Runnable-grensesnittet til konstruktøren av Thread-klassen:

Offentlig klasse Program // Klasse med hoved() metode. (public static void main (String args) (// Opprett en tråd Thread myThready = new Thread (new Runnable () (public void run () // Denne metoden vil kjøre på en sidethread (System.out.println ("Hei) fra en sidetråd! ");))); myThready.start (); // Start tråden System.out.println (" Hovedtråden er ferdig ... ");))

Metode 2
Opprett en etterkommer av Thread-klassen og overstyr dens kjøre()-metode:

Class AffableThread forlenger tråden (@Override public void run () // Denne metoden vil bli utført på en sidethread (System.out.println ("Hei fra en side thread!");)) Public class Program (statisk AffableThread mSecondThread; public static void main (String args) (mSecondThread = new AffableThread (); // Opprett tråd mSecondThread.start (); // Start tråd System.out.println ("Hovedtråd ferdig ...");))

I eksemplet ovenfor blir en annen tråd opprettet og startet i hovedmetoden (). Det er viktig å merke seg at etter å ha kalt mSecondThread.start ()-metoden, fortsetter hovedtråden sin utførelse uten å vente på at tråden den skapte skal fullføres. Og instruksjonene som følger kallet til start()-metoden vil bli utført parallelt med instruksjonene til mSecondThread-tråden.

For å demonstrere hvordan tråder fungerer parallelt, la oss vurdere et program der to tråder krangler om det filosofiske spørsmålet "hva kom først, et egg eller en kylling?" Hovedtråden er sikker på at kyllingen var den første, som han vil rapportere hvert sekund. Den andre strømmen, en gang i sekundet, vil motbevise motstanderen. Den totale tvisten vil vare i 5 sekunder. Strømmen som vil være den siste til å gi sitt svar på dette utvilsomt brennende filosofiske spørsmålet, vil vinne. Eksemplet bruker funksjoner som ennå ikke er nevnt (isAlive () sleep () og join ()). Det gis kommentarer til dem, og de vil bli diskutert mer detaljert senere.

Class EggVoice utvider tråden (@Override public void run () (for (int i = 0; i< 5; i++) { try{ sleep(1000); //Приостанавливает поток на 1 секунду }catch(InterruptedException e){} System.out.println("яйцо!"); } //Слово «яйцо» сказано 5 раз } } public class ChickenVoice //Класс с методом main() { static EggVoice mAnotherOpinion; //Побочный поток public static void main(String args) { mAnotherOpinion = new EggVoice(); //Создание потока System.out.println("Спор начат..."); mAnotherOpinion.start(); //Запуск потока for(int i = 0; i < 5; i++) { try{ Thread.sleep(1000); //Приостанавливает поток на 1 секунду }catch(InterruptedException e){} System.out.println("курица!"); } //Слово «курица» сказано 5 раз if(mAnotherOpinion.isAlive()) //Если оппонент еще не сказал последнее слово { try{ mAnotherOpinion.join(); //Подождать пока оппонент закончит высказываться. }catch(InterruptedException e){} System.out.println("Первым появилось яйцо!"); } else //если оппонент уже закончил высказываться { System.out.println("Первой появилась курица!"); } System.out.println("Спор закончен!"); } } Консоль: Спор начат... курица! яйцо! яйцо! курица! яйцо! курица! яйцо! курица! яйцо! курица! Первой появилась курица! Спор закончен!

I det gitte eksemplet sender to strømmer parallelt i 5 sekunder ut informasjon til konsollen. Det er umulig å forutsi nøyaktig hvilken strøm som ender opp med å snakke sist. Du kan prøve, og du kan til og med gjette, men det er stor sannsynlighet for at det samme programmet vil ha en annen «vinner» neste gang det lanseres. Dette er på grunn av den såkalte "asynkrone kodekjøringen". Asynkroni betyr at du ikke kan si at noen instruksjoner fra en tråd vil kjøre tidligere eller senere enn instruksjoner fra en annen. Eller, med andre ord, parallelle tråder er uavhengige av hverandre, bortsett fra de tilfellene hvor programmereren selv beskriver avhengighetene mellom tråder ved å bruke språkfasilitetene som er gitt for dette.

Nå litt om avslutning av prosesser ...

Prosessavslutning og demoner

I Java slutter en prosess når den siste tråden slutter. Selv om hovedmetoden () allerede er fullført, men trådene den skaper fortsatt kjører, vil systemet vente på at de skal fullføres.

Denne regelen gjelder imidlertid ikke for en spesiell type tråd - demoner. Hvis den siste normale tråden i prosessen gikk ut og bare daemon-trådene gjenstår, vil de bli tvangsavbrutt og prosessen vil avsluttes. Oftest brukes demon-tråder til å utføre bakgrunnsoppgaver som tjener en prosess i løpet av dens levetid.

Å erklære en tråd som en demon er ganske enkelt - du må kalle metoden før du starter tråden setDaemon (true);
Du kan sjekke om en tråd er en demon ved å kalle dens metode boolsk er Daemon ();

Avsluttende strømmer

Java har (eksistert) midler for å tvinge en tråd til å avslutte. Spesielt avslutter Thread.stop ()-metoden tråden umiddelbart etter at den er utført. Imidlertid er denne metoden, så vel som Thread.suspend (), som suspenderer tråden, og Thread.resume (), som fortsetter utførelsen av tråden, avviklet og bruken av dem er nå sterkt frarådet. Faktum er at tråden kan "drepes" under utførelsen av en operasjon, hvis avbrudd i midten av et ord vil etterlate et objekt i feil tilstand, noe som vil føre til en vanskelig å fange og tilfeldig forekommende feil.

I stedet for tvungen avslutning av en tråd, brukes et opplegg der hver tråd er ansvarlig for sin egen avslutning. Tråden kan stoppe enten når den er ferdig med å kjøre ()-metoden, (hoved () - for hovedtråden), eller på et signal fra en annen tråd. Og hvordan man reagerer på et slikt signal er igjen et spørsmål om selve flyten. Etter å ha mottatt den, kan tråden utføre noen operasjoner og fullføre kjøringen, eller den kan ignorere den helt og fortsette å kjøre. Det er programmererens ansvar å beskrive reaksjonen på trådavslutningssignalet.

Java har en innebygd trådvarslingsmekanisme kalt Interruption, som vi skal dekke om kort tid, men først ta en titt på følgende program:

Incremenator er en tråd som hvert sekund legger til eller trekker en fra verdien av den statiske variabelen Program.mValue. Incremenator inneholder to private felt, mIsIncrement og mFinish. Hvilken handling som utføres avgjøres av den boolske variabelen mIsIncrement - hvis den er sann, utføres addisjon av en, ellers - subtraksjon. Og slutten av strømmen skjer når verdien til mFinish blir sann.

Class Incremenator utvider tråden (// Om søkeordet volatile - like under private volatile boolean mIsIncrement = true; private volatile boolean mFinish = false; public void changeAction () // Reverse the action (mIsIncrement =! MIsIncrement;) public void finish ( ) // Starter trådavslutning (mFinish = true;) @Override public void run () (do (if (! MFinish) // Check if (! MFinish) // Sjekk om det må avsluttes (if (mIsIncrement) Program.mValue ++; // Increment else Program.mValue- -; // Decrement // Skriv ut gjeldende verdi av variabelen System.out.print (Program.mValue + "");) else return; // Avslutt tråden prøv ( Thread.sleep (1000); // Suspend the thread for 1 sek. ) catch (InterruptedException e) ()) while (true);)) public class Program (// Variabel operert av inkrementatoren public static int mValue = 0; static Incremenator mInc; // Objekt for sidestrømmen offentlig static void main (String args ) (mInc = new Incremenator (); // Opprett strøm S system.out.print ("Verdi ="); mInc.start (); // Start tråden // Tredobbel endring av inkrementatorhandlingen // med et intervall på i * 2 sekunder for (int i = 1; i<= 3; i++) { try{ Thread.sleep(i*2*1000); //Ожидание в течении i*2 сек. }catch(InterruptedException e){} mInc.changeAction(); //Переключение действия } mInc.finish(); //Инициация завершения побочного потока } } Консоль: Значение = 1 2 1 0 -1 -2 -1 0 1 2 3 4

Du kan samhandle med strømmen ved å bruke changeAction ()-metoden (for å endre subtraksjon til addisjon og omvendt) og finish ()-metoden (for å avslutte strømmen).

Nøkkelordet volatile ble brukt i deklarasjonene av variablene mIsIncrement og mFinish. Den må brukes for variabler som brukes av forskjellige tråder. Dette er fordi verdien av en variabel som er erklært uten flyktig, kan bufres separat for hver tråd, og verdien fra denne hurtigbufferen kan være forskjellig for hver tråd. Å erklære en variabel med det flyktige nøkkelordet deaktiverer slik caching for den, og alle forespørsler til variabelen vil bli sendt direkte til minnet.

Dette eksemplet viser hvordan du kan organisere kommunikasjon mellom tråder. Det er imidlertid ett problem med denne tilnærmingen til trådavslutning - Incremenator sjekker verdien av mFinish-feltet én gang per sekund, slik at det kan ta opptil et sekund mellom tidspunktet finish ()-metoden utføres og den faktiske avslutningen av tråd. Det ville være flott om, etter å ha mottatt et signal fra utsiden, sleep ()-metoden ville returnere utførelse og tråden umiddelbart ville begynne å avslutte. For å oppnå dette scenariet er det et innebygd trådvarsel kalt avbrudd.

Avbrudd

Thread-klassen inneholder et skjult boolsk felt, som ligner på mFinish-feltet i Incremenator-programmet, kalt avbruddsflagget. Dette flagget kan settes ved å kalle trådens avbruddsmetode (). Det er to måter å sjekke om dette flagget er satt. Den første måten er å kalle bool isInterrupted ()-metoden til trådobjektet, den andre er å kalle bool static-metoden Thread.interrupted (). Den første metoden returnerer tilstanden til avbruddsflagget og lar det flagget være urørt. Den andre metoden returnerer statusen til flagget og tilbakestiller det. Merk at Thread.interrupted () er en statisk metode av Thread-klassen, og å kalle den returnerer verdien til avbruddsflagget til tråden den ble kalt fra. Derfor kalles denne metoden bare fra tråden og lar tråden sjekke avbruddstilstanden.

Så tilbake til programmet vårt. Avbruddsmekanismen vil tillate oss å løse problemet med at tråden sovner. Metoder som suspenderer kjøringen av en tråd, for eksempel sleep (), vent () og join (), har en særegenhet - hvis avbruddsmetoden () for denne tråden kalles under kjøringen, vil de kaste et InterruptedException uten å vente på tidsavbruddet for å utløpe.

La oss lage Incremenator-programmet på nytt - nå, i stedet for å avslutte tråden ved å bruke finish ()-metoden, vil vi bruke standard avbruddsmetoden (). Og i stedet for å sjekke mFinish-flagget, vil vi kalle bool-metoden Thread.interrupted ();
Slik vil Incremenator-klassen se ut etter å ha lagt til avbruddsstøtte:

Class Incremenator forlenger tråden (privat flyktig boolesk mIsIncrement = true; public void changeAction () // Reverse the action (mIsIncrement =! MIsIncrement;) @Override public void run () (do (hvis (! Thread.interrupted ())) / / Avbryt sjekk (if (mIsIncrement) Program.mValue ++; // Increment else Program.mValue--; // Decrement // Skriv ut gjeldende verdi av variabelen System.out.print (Program.mValue + "");) else return; / / Avslutt tråden prøv (Thread.sleep (1000); // Suspend tråden i 1 sek.) Catch (InterruptedException e) (retur; // Avslutt tråden etter avbrudd)) mens (true);) ) klasse Program (// Variabel drevet av inkrementatoren offentlig statisk int mValue = 0; static Incremenator mInc; // Objekt for sidestrømmen offentlig statisk void hoved (String args) (mInc = new Incremenator (); // Create stream System .out.print ("Verdi =" ); mInc.start (); // Start tråden // Tredobbel endring av inkrementatorhandlingen // med et intervall m i i * 2 sekunder for (int i = 1; Jeg<= 3; i++) { try{ Thread.sleep(i*2*1000); //Ожидание в течении i*2 сек. }catch(InterruptedException e){} mInc.changeAction(); //Переключение действия } mInc.interrupt(); //Прерывание побочного потока } } Консоль: Значение = 1 2 1 0 -1 -2 -1 0 1 2 3 4

Som du kan se, ble vi kvitt finish ()-metoden og implementerte den samme trådtermineringsmekanismen ved å bruke det innebygde avbruddssystemet. I denne implementeringen har vi en fordel - sleep ()-metoden vil returnere (kaste et unntak) umiddelbart etter at tråden er avbrutt.

Merk at sleep () og join () metodene er pakket inn i try-catch. Dette er en forutsetning for at disse metodene skal fungere. Anropskoden må fange avbrutt unntak som de kaster når de blir avbrutt mens de venter.

Vi fant ut starten og slutten av tråder, så vil jeg snakke om metodene som brukes når du arbeider med tråder.

Thread.sleep ()-metoden

Thread.sleep () er en statisk metode av Thread-klassen som stopper utførelsen av tråden den ble kalt opp i. Under kjøringen av sleep ()-metoden, slutter systemet å allokere prosessortid til tråden, og distribuerer den blant andre tråder. Sleep ()-metoden kan enten kjøres i en spesifisert tidsperiode (millisekunder eller nanosekunder) eller til den stoppes av et avbrudd (i så fall vil den kaste et InterruptedException).

Tråd.søvn (1500); // Vent halvannet sekund Thread.sleep (2000, 100); // Vent 2 sekunder og 100 nanosekunder

Selv om søvn ()-metoden kan ta nanosekunder som en time-out, bør du ikke ta lett på det. I mange systemer er ventetiden fortsatt avrundet til millisekunder eller til og med titalls millisekunder.

Yield () metode

Den statiske metoden Thread.yield () får prosessoren til å bytte til å behandle andre tråder i systemet. Metoden kan for eksempel være nyttig når en tråd venter på at en hendelse skal inntreffe, og det er nødvendig å sjekke for dens forekomst så ofte som mulig. I dette tilfellet kan du legge hendelsessjekken og Thread.yield ()-metoden i en løkke:

// Venter på at en melding kommer mens (! MsgQueue.hasMessages ()) // Mens det ikke er noen meldinger i køen (Thread.yield (); // Overfør kontroll til andre tråder)

Bli med ()-metoden

Java gir en mekanisme for at en tråd kan vente på at en annen skal fullføres. Metoden join () brukes til dette. For å få hovedtråden til å vente på at sidetråden myThready skal fullføres, må du for eksempel utføre myThready.join ()-setningen på hovedtråden. Når myThready-tråden er ferdig, vil join ()-metoden returnere og hovedtråden kan fortsette kjøringen.

Metoden join () har en overbelastning som tar en timeout som parameter. I dette tilfellet returnerer join () enten når den ventede tråden er ferdig eller tidsavbrutt. I likhet med Thread.sleep ()-metoden, kan join-metoden vente i millisekunder og nanosekunder – argumentene er de samme.

Ved å angi tidsavbruddet for tråden kan du for eksempel oppdatere et animert bilde mens hovedtråden (eller en annen) venter på fullføringen av en sidetråd som utfører ressurskrevende operasjoner:

Tenkerhjerne = nytenker (); // Thinker er en etterkommer av trådklassen. hjerne.start (); // Begynn å "tenke". do (mThinkIndicator.refresh (); // mThinkIndicator er et animert bilde.try (brain.join (250); // Vent til tanken tar slutt i et kvarters tid.) catch (InterruptedException e) ()) mens (brain.isAlive ()); // Mens hjernen tenker ... // har hjernen tenkt ferdig (det er stående applaus).

I dette eksempelet tenker hjernestrømmen på noe og det antas at det tar lang tid. Hovedtråden venter på den i et kvart sekund, og hvis denne tiden for refleksjon ikke er nok, oppdaterer den "refleksjonsindikatoren" (noen animert bilde). Som et resultat, mens han tenker, observerer brukeren en indikator på tankeprosessen på skjermen, som lar ham vite at de elektroniske hjernene er opptatt med noe.

Strømprioriteringer

Hver tråd i systemet har sin egen prioritet. Prioritet er et tall i et trådobjekt, en høyere verdi betyr en høyere prioritet. Systemet kjører de høyere prioriterte trådene først, og de lavere prioriterte trådene får kun CPU-tid når deres mer privilegerte søsken er inaktive.

Du kan jobbe med trådprioriteter ved å bruke to funksjoner:

void setPriority (int priority)- setter prioritet til tråden.
Mulige prioritetsverdier er MIN_PRIORITY, NORM_PRIORITY og MAX_PRIORITY.

int getPriority ()- får prioritet til tråden.

Noen nyttige metoder i trådklassen

Det er stort sett det. Til slutt, her er noen nyttige trådteknikker.

boolesk er alive ()- returnerer true hvis myThready () kjører og usann hvis tråden ikke er startet ennå eller har blitt avsluttet.

setName (String threadName)- Angir navnet på strømmen.
String getName ()- Får navnet på strømmen.
Navnet på en tråd er en streng knyttet til den, som i noen tilfeller hjelper til med å forstå hvilken tråd som utfører en eller annen handling. Dette er noen ganger nyttig.

statisk tråd Thread.currentThread ()- en statisk metode som returnerer trådobjektet der det ble kalt.

lang getId ()- returnerer strømidentifikatoren. ID er et unikt nummer som er tildelt strømmen.

Konklusjon

Merk at artikkelen ikke dekker alle nyansene ved flertrådsprogrammering. Og koden gitt i eksemplene mangler noen nyanser for fullstendig korrekthet. Spesielt eksemplene bruker ikke synkronisering. Synkronisering av tråder er et emne uten læring som du ikke vil kunne programmere riktige flertrådede applikasjoner. Du kan lese om det for eksempel i boken "Java Concurrency in Practice" eller