Du kan skapa flertrådade applikationer i Java. Tråd och körbar, vad ska man välja? Vad är multithreading

Att skriva parallell kod är inte en lätt uppgift, och det är ännu svårare att kontrollera dess korrekthet. Även om Java ger omfattande stöd för multithreading och synkronisering på språk- och API-nivå, visar det sig i verkligheten att skrivande av korrekt multitrådad Java-kod beror på den individuella programmerarens erfarenhet och flit. Nedan finns en uppsättning tips som hjälper dig att förbättra nivån på din flertrådiga Java-kod. Vissa av er kanske redan är bekanta med dessa tips, men det skadar aldrig att fräscha upp dem vartannat år.

Många av dessa tips kom från lärande och praktisk programmering, och även efter att ha läst böckerna "Java concurrency in practice" och "Effective Java". Jag råder alla Java-programmerare att läsa den första två gånger; ja, det stämmer, TVÅ GÅNGER. Parallellism är ett förvirrande och svårt ämne att förstå (som rekursion för vissa), och efter att ha läst det en gång kanske du inte förstår allt.

Det enda syftet med att använda parallellism är att skapa skalbara och snabba applikationer, men du bör alltid komma ihåg att hastigheten inte bör bli ett hinder för korrekthet. Ditt Java-program måste uppfylla sin invariation om det körs entrådigt eller flertrådigt. Om du är ny på parallell programmering, kolla först olika problem, uppstår när parallell lansering program (till exempel: dödläge, tävlingsförhållanden, resurshunger, etc.).

1. Använd lokala variabler

Försök alltid att använda lokala variabler istället för klassfält eller statiska fält. Ibland använder utvecklare klassfält för att spara minne och återanvända variabler, och räknar ut att skapa en lokal variabel varje gång en metod anropas kan kräva stor kvantitet extra minne. Ett exempel på denna användning skulle vara en samling som deklareras som ett statiskt fält och återanvänds med metoden clear(). Detta försätter klassen i ett delat tillstånd, vilket den inte borde ha, eftersom ursprungligen skapad för parallell användning i flera trådar. I koden nedan anropas metoden execute() från olika trådar, och en temporär samling krävdes för att implementera den nya funktionen. I originalkod en statisk samling (List) användes, och utvecklarens avsikt var tydlig - att rensa samlingen i slutet av metoden execute() så att den kunde återanvändas senare. Utvecklaren trodde att hans kod var trådsäker eftersom CopyOnWriteArrayList var trådsäker. Men detta är inte sant - metoden execute() anropas från olika trådar, och en av trådarna kan komma åt data som skrivits av en annan tråd för att gemensam lista. Synkronisering tillhandahålls av CopyOnWriteArrayList i I detta fallär inte tillräckligt för att säkerställa invariansen för metoden execute().

Public static class ConcurrentTask (privat static List temp = Collections.synchronizedList(new ArrayList()); @Override public void execute(Meddelandemeddelande) ( // Använd en lokal temporär lista // List temp = new ArrayList(); // Lägg till att lista något från meddelandet temp.add("meddelande.getId()");

Problem: Data från ett meddelande kommer att hamna i ett annat om två anrop till execute() "skär varandra", dvs. den första tråden kommer att lägga till ID från det första meddelandet, sedan kommer den andra tråden att lägga till ID från det andra meddelandet (detta kommer att hända innan listan rensas), så data för ett av meddelandena kommer att skadas.

Lösningsalternativ:

  1. Lägg till ett synkroniseringsblock till den del av koden där tråden lägger till något i den tillfälliga listan och rensar den. På så sätt kommer en annan tråd inte att kunna komma åt listan förrän den första tråden har slutat arbeta med den. I det här fallet kommer denna del av koden att vara enkeltrådad, vilket kommer att minska applikationens prestanda som helhet.
  2. Använd en lokal lista i stället för ett klassfält. Ja, detta kommer att öka minneskostnaderna, men det kommer att bli av med synkroniseringsblocket och göra koden mer läsbar. Dessutom behöver du inte oroa dig för tillfälliga föremål - sopsamlaren tar hand om dem.

Endast ett av fallen presenteras här, men när jag skriver parallell kod föredrar jag personligen lokala fältvariabler klass, om de senare inte krävs av applikationsarkitekturen.

2. Föredrar oföränderliga klasser framför föränderliga

Den mest kända metoden inom flertrådsprogrammering i Java är användningen av oföränderlig (oföränderlig) klasser. Oföränderliga klasser som String, Integer och andra gör det lättare att skriva parallell kod i Java eftersom du behöver inte oroa dig för tillståndet för objekt i dessa klasser. Oföränderliga klasser minskar antalet synkroniseringselement i din kod. Ett objekt av en oföränderlig klass, när det väl har skapats, kan inte ändras. Det bästa exemplet på en sådan klass är string (java.lang.String). Alla strängändringsoperationer i Java (versaler, delsträngar, etc.) kommer att resultera i skapandet av ett nytt strängobjekt för resultatet av operationen, vilket lämnar den ursprungliga strängen orörd.

3. Minska synkroniseringsområden

Någon kod inom synkroniseringsregionen kan inte exekveras parallellt, och om 5 % av ditt program är i synkroniseringsblock, så kan, enligt Amdahls lag, prestandan för hela applikationen inte förbättras mer än 20 gånger. främsta orsaken Detta beror på att 5 % av koden alltid exekveras sekventiellt. Du kan minska detta antal genom att minska synkroniseringsområden - försök att använda dem endast för kritiska avsnitt. Bästa exemplet s- dubbelkolla låsning, som kan implementeras i Java 1.5 och senare med hjälp av flyktiga variabler.

4. Använd en trådpool

Skapar en tråd (Tråd)- dyr operation. Om du vill skapa en skalbar Java-applikation måste du använda en trådpool. Förutom tyngden av skapande operation genererar manuell trådhantering en hel del repetitiv kod, vilket, blandat med affärslogik, minskar läsbarheten för koden som helhet. Trådhantering är ramverkets uppgift, oavsett om det är ett Java-verktyg eller något annat verktyg du vill använda. JDK har ett välorganiserat, rikt och fullt testat ramverk känt som Executor-ramverket, som kan användas varhelst en trådpool krävs.

5. Använd synkroniseringsverktyg istället för wait() och notify()

Java 1.5 introducerade många synkroniseringsverktyg som CyclicBarrier, CountDownLatch och Semaphore. Du bör alltid först titta på vad JDK har för synkronisering innan du använder wait() och notify() . Det kommer att vara mycket lättare att implementera reader-writer-mönstret med BlockingQueue än med wait() och notify() . Det blir också mycket lättare att vänta på 5 trådar för att slutföra beräkningen med CountDownLatch istället för att implementera samma sak via wait() och notify() . Utforska java.util.concurrent-paketet för att skriva samtidig kod i Java är bäst sätt.

6. Använd BlockingQueue för att implementera Producer-Consumer

Detta råd följer på det föregående, men jag har lyft fram det separat på grund av dess betydelse för parallella tillämpningar, används i den verkliga världen. Lösningen på många flertrådsproblem är baserad på Producer-Consumer-mönstret och BlockingQueue - Det bästa sättet implementera det i Java. Till skillnad från Exchanger, som kan användas för en författare och läsare, kan BlockingQueue användas för korrekt bearbetning flera skribenter och läsare.

7. Använd trådsäkra samlingar istället för att blockera samlingar

Trådsäkra samlingar ger större skalbarhet och prestanda än deras blockerande motsvarigheter (Collections.synchronizedCollection, etc.). ConcurrentHashMap, som enligt min mening är den mest populära trådsäkra samlingen, uppvisar bättre prestanda än att blockera HashMap eller Hashtable när det finns fler läsare än skribenter. En annan fördel med trådsäkra samlingar är att de implementeras med en ny låsmekanism (java.util.concurrent.locks.Lock) och använder de inbyggda synkroniseringsmekanismerna som tillhandahålls av den underliggande hårdvaran och JVM. Använd dessutom CopyOnWriteArrayList istället för Collections.synchronizedList om läsning från listan sker oftare än att ändra den.

8. Använd semaforer för att skapa begränsningar

För att skapa ett tillförlitligt och stabilt system måste du ha restriktioner på resurser (databaser, filsystem, uttag, etc.). Din kod ska aldrig skapa och/eller använda en oändlig mängd resurser. Semaforer (java.util.concurrent.Semaphore) - ett bra val för att skapa begränsningar för användningen av dyra resurser som databasanslutningar (förresten, i det här fallet kan du använda en anslutningspool). Semaforer hjälper till att skapa begränsningar och blockera trådar om en resurs inte är tillgänglig.

9. Använd synkroniseringsblock istället för blockeringsmetoder

Detta tips expanderar på spetsen för att minska synkroniseringsområden. Att använda synkroniseringsblock är en metod för att minska synkroniseringsområdet, vilket också låter dig låsa på ett annat objekt än det nuvarande som representeras av denna pekare. Den första kandidaten bör vara en atomvariabel, sedan en volatil variabel om de uppfyller dina synkroniseringskrav. Om du behöver ömsesidig uteslutning, använd antingen ett ReentrantLock eller ett synkroniserat block först. Om du är ny på parallell programmering och inte utvecklar något vital viktig applikation, du kan bara använda ett synkroniserat block - det blir säkrare och enklare.

10. Undvik att använda statiska variabler

Som visas i det första tipset kan statiska variabler orsaka många problem när de används i parallell kod. Om du använder en statisk variabel, se till att det är en konstant eller en oföränderlig samling. Om du funderar på att återanvända en samling för att spara minne, titta tillbaka på det första tipset.

11. Använd lås istället för synkroniserat

Detta sista bonustips bör användas med försiktighet. Gränssnittslås - kraftfullt verktyg, men hans makt medför också stort ansvar. Olika föremål Lås vid läsning och skrivning gör att du kan implementera skalbara datastrukturer som ConcurrentHashMap, men kräver stor noggrannhet vid programmering. Till skillnad från ett synkroniserat block släpper inte tråden låset automatiskt. Du måste uttryckligen anropa unlock() för att släppa låset. Det är bra att anropa den här metoden i ett slutgiltigt block så att blockeringen upphör under alla förhållanden:

Lock.lock(); försök ( //gör något ... ) slutligen ( lock.unlock(); )

Slutsats

Du har fått tips för att skriva flertrådig kod i Java. Återigen skadar det aldrig att läsa om "Java concurrency in practice" och "Effective Java" då och då. Du kan också utveckla det nödvändiga sättet att tänka för parallell programmering genom att helt enkelt läsa andras kod och försöka visualisera problemen under utvecklingen. Slutligen, fråga dig själv vilka regler du följer när du utvecklar flertrådade applikationer i Java?

I teoretiska arbeten om multithreading kan du hitta en beskrivning av tre problem som, enligt författarna, täcker alla möjliga multithreading-problem - producent-konsumentproblemet, läsare-skribentproblemet och lunchfilosofernas problem. Allegorin är vacker och ganska bra på sitt sätt, men enligt min mening, för en bräcklig ung programmerare, säger den ingenting alls. Därför kommer jag att beskriva problemen från mitt klocktorn. Det finns bara två problem.

Det första problemet är att komma åt en resurs från flera trådar. Vi har redan beskrivit problemet med en spade. Du kan utöka alternativet - det finns en vattentank (med en kran), 25 törstiga gruvarbetare och 5 muggar för alla. Vi måste komma överens, annars kan dödandet börja. Dessutom är det nödvändigt att inte bara hålla muggarna intakta, utan också att organisera allt så att alla kan dricka. Detta hänför sig delvis till problem nummer två.
Det andra problemet är interaktionssynkronisering. En gång erbjöds jag uppgiften att skriva ett enkelt program att ha två trådar som spelar pingis. Den ena skriver "Ping" och den andre skriver "Pong". Men de måste göra detta en efter en. Låt oss nu föreställa oss att vi måste göra samma uppgift, men med 4 trådar - låt oss spela par-på-par.

De där. Formuleringen av problem är ganska enkel. En gång – vi behöver organisera en välordnad och säker åtkomst till den delade resursen. Två - du måste köra trådarna i någon ordning.
Det är en fråga om genomförande. Och här ställs vi inför många svårigheter, som de pratar om med förväntan (och kanske av goda skäl). Låt oss börja med den delade resursen.

Delad resurs över flera trådar

Jag föreslår att omedelbart demonstrera problemet med ett enkelt exempel. Dess uppgift är att lansera 200 klasstrådar Mottråd. Varje tråd får en länk till en enda föremål Disken. Under körningen anropar tråden en metod för detta objekt öka Räknaren tusen gånger. Metoden ökar variabeln disken med 1. Efter att ha lanserat 200 trådar väntar vi på att de ska ta slut (vi går bara och lägger oss i 1 sekund - det räcker). Och på slutet skriver vi ut resultatet. Titta på koden - enligt min mening är allt ganska transparent där:

<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 klass CounterTester

för (int i = 0 ; i< 200 ; i ++ ) {

ct. Start();

Tråd. sömn(1000);

klassräknare

privat lång räknare = 0L;

public void ökningCounter() (

räknare++;

public long getCounter() (

returräknare ;

privat diskdisk ;

detta. räknare = räknare ;

@Åsidosätta

public void run() (

för (int i = 0 ; i< 1000 ; i ++ ) {

Logiskt sett borde vi få följande resultat - 200 trådar med 1000 tillägg = 200000. Men, fasansfulla fasor, detta är inte alls sant. Mina resultat varierar, men uppenbarligen inte 200 000 Vad är problemet? Problemet är att vi försöker anropa en metod från 200 trådar samtidigt öka Räknaren. Vid första anblicken händer inget hemskt i det - vi lägger helt enkelt till variabeln disken enhet. Vad är det som är så hemskt med det här?
Det hemska är att den till synes ofarliga koden för att lägga till en faktiskt körs i mer än ett steg. Först läser vi in ​​värdet på variabeln i registret, lägger sedan till en till den och skriver sedan tillbaka resultatet till variabeln. Som du kan se finns det mer än ett steg (i hemlighet finns det till och med fler än de tre som jag beskrev). Och föreställ dig nu att två trådar (eller ännu fler) samtidigt läser av värdet på en variabel - till exempel fanns det ett värde på 99. Nu lägger båda trådarna ett till 99, båda får 100 och båda skriver detta värde till variabeln. Vad händer där? Det är lätt att se att det blir 100. Men det borde vara 101. Det kan vara ännu värre om någon tråd "lyckades" räkna 98 och "fastnade" i kön av trådar för att köras. Då får vi inte ens 100. Problem :)

Delad resursåtkomst är ett av de största problemen med multithreading. För hon är väldigt lömsk. Du kan göra allt mycket tillförlitligt, men då sjunker prestandan. Och så fort du ger "slack" (medvetet, för produktivitetens skull), kommer en situation oundvikligen att uppstå där "slacket" kommer ut i all sin glans.

Det magiska ordet är synkroniserat

Vad kan göras för att bli av med den situation vi befinner oss i med våra underbara flöden. Låt oss tänka lite först. När vi kommer till butiken går vi till kassan för att betala. Kassörskan servar endast en person åt gången. Vi ställer upp alla för henne. I huvudsak blir kassaregistret en exklusiv resurs som endast kan användas av en kund åt gången. Multithreading erbjuder exakt samma metod - du kan definiera en resurs som exklusivt tillhandahållen till endast en tråd åt gången. Denna resurs kallas en "monitor". Detta är det vanligaste objektet som en tråd måste "fånga". Alla trådar som vill komma åt denna monitor (objekt) är i kö. Dessutom behöver du inte skriva någon speciell kod för detta - försök bara att "fånga" skärmen. Men hur definierar vi detta? Låt oss ta reda på det.
Jag föreslår att du kör vårt exempel, men med ytterligare ett ord i metodbeskrivningen öka Räknaren- det här ordet synkroniserad.

paket edu.javacourse.counter; public class CounterTester ( public static void main(String args) kastar 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(); } } }

paket edu. javakurs. disken ;

offentlig klass CounterTester

public static void main(String args) kastar InterruptedException(

Räknare = ny Räknare ();

för (int i = 0 ; i< 200 ; i ++ ) {

CounterThread ct = new CounterThread(counter);

ct. Start();

Tråd. sömn(1000);

Systemet. ut . println ("Räknare:" + räknare . getCounter () );

klassräknare

privat lång räknare = 0L;

public synchronized void increaseCounter() (

räknare++;

public long getCounter() (

returräknare ;

klass CounterThread förlänger tråden

privat diskdisk ;

offentlig CounterThread (Räknarräknare) (

detta. räknare = räknare ;

@Åsidosätta

public void run() (

för (int i = 0 ; i< 1000 ; i ++ ) {

disken. ökaCounter();

Och... se och se. Allt fungerade. Vi får det förväntade resultatet - 200000. Vad gör detta magiska ord - synkroniserad ?
Ord synkroniserad säger att innan en tråd kan anropa den här metoden på vårt objekt måste den "fånga" vårt objekt och sedan köra den önskade metoden. Än en gång och försiktigt (ibland föreslås ett lite annorlunda tillvägagångssätt, vilket enligt min mening är extremt farligt och felaktigt - jag kommer att beskriva det lite senare) - först "fångar" tråden (låser - från ordet lås - lås, block) monitorobjektet (i vårt fall är det klassobjekt Disken) och först efter det kommer tråden att kunna köra metoden öka Räknaren. Exklusivt, helt ensam utan konkurrenter.
Det finns en annan tolkning synkroniserad, vilket kan vara missvisande - det låter ungefär så här: i en synkroniserad metod kan flera trådar inte komma in samtidigt. Det är inte sant. För då visar det sig att om en klass har flera metoder synkroniserad, då kan du samtidigt köra två olika metoder för samma objekt, markerade som synkroniserad. Det är inte sant. Om en klass har 2, 3 eller fler metoder synkroniserad, när åtminstone en exekveras blockeras hela objektet. Detta innebär att alla metoder betecknas som synkroniserad inte tillgänglig för andra trådar. Om metoden inte är betecknad som sådan. då är det inga problem - gör det för din hälsa.
Och återigen - först "fångade de", sedan utförde de metoden, sedan "släppte de". Nu är föremålet fritt och den som var först med att fånga det från strömmarna har rätt.
Om metoden deklareras som statisk, då blir hela klassen monitorobjektet och åtkomst till den blockeras på nivån för alla objekt i denna klass.

När de diskuterade artikeln påpekade de en felaktighet som jag medvetet gjorde (för enkelhetens skull), men det är nog vettigt att nämna det. Det handlar om metoden getCounter. Strängt taget bör den också betecknas som synkroniserad, för i samma ögonblick som vår variabel ändras, kommer någon annan tråd att vilja läsa den. Och för att undvika problem måste åtkomst till denna variabel synkroniseras i alla metoder.
Fast vad gäller getCounter, då kan du här använda en ännu mer intressant funktion - operationernas atomicitet. Du kan läsa om det i Atomic access-artikeln. Huvudtanken är att läsning och skrivning av vissa elementära typer och referenser sker i ett steg och är i princip säkert. Om fältet disken det var till exempel int, då skulle det vara möjligt att läsa inte i en synkron metod. För typ lång Och dubbel vi måste deklarera en variabel disken Hur flyktig. Varför detta kan vara intressant – det måste vi ta hänsyn till int består av 4 byte och du kan föreställa dig situationen att numret inte kommer att skrivas i ett steg. Men detta är rent teoretiskt - JVM garanterar oss att läsning och skrivning av den elementära typen int görs i ett steg och inte en enda tråd kan komma in i denna operation och förstöra något.

Det finns ett annat sätt att använda ordet synkroniserad- inte i beskrivningen av metoden, utan i koden. Låt oss ändra vårt exempel igen i metoddelen öka Räknaren.

paket edu.javacourse.counter; public class CounterTester ( public static void main(String args) kastar 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(); } } }

paket edu. javakurs. disken ;

offentlig klass CounterTester

public static void main(String args) kastar InterruptedException(

Räknare = ny Räknare ();

för (int i = 0 ; i< 200 ; i ++ ) {

Innan vi lär oss om Java-trådar, låt oss titta in i den närmaste framtiden. Föreställ dig att du skickade in ditt CV och hade en intervju. Du och ett par dussin framtida kollegor har blivit inbjudna att arbeta på ett stort mjukvaruföretag. Bland annat krångel behöver du lämna in pappersdokument för anställning till en trött HR-anställd.

För att påskynda processen kan sökande till tjänsten delas in i två grupper och fördelas mellan två HR-chefer (om det finns några i företaget). Som ett resultat får vi processacceleration på grund av parallell ( parallell) designarbete.

Om det bara finns en personalhandläggare i företaget, då måste du på något sätt ta dig ut. Till exempel kan du återigen dela in alla i två grupper, till exempel intervjua tjejer och killar i tur och ordning.

Eller enligt en annan princip: eftersom det är fler i den lägre gruppen kommer vi att varva två tjejer med en kille.

Detta sätt att organisera arbetet kallas flertrådig. Vår trötta HR-ansvarig byter till olika grupper för att rekrytera nästa medarbetare från dem. Det finns kanske elva grupper och fyra personalofficerare. I det här fallet, flertrådig ( multitrådning) bearbetning kommer att ske parallellt av flera personalansvariga, som kan ta nästa person från vilken grupp som helst för att behandla hans dokument.

Processer

Bearbeta ( bearbeta) i detta fall kommer det att organiseras arbetet för att ta emot dokument. I en organisation kan flera processer urskiljas: redovisning, mjukvaruutveckling, möten med kunder, lagerdrift etc. Resurser tilldelas för varje process: lokaler, anställda för dess utförande. Processerna är isolerade från varandra: HR-ansvariga har inte tillgång till redovisningsdatabasen och kundtjänstchefer springer inte runt i lagret. Om en process behöver få tillgång till någon annans resurser är det nödvändigt att etablera kommunikation mellan processer: PM, gemensamma möten.

Strömmar

Arbetet i processen är organiserat i form av trådar (java-tråd). För HR-avdelningen är flöde organiseringen av arbetet för att betjäna en grupp. På den allra första bilden finns ett flöde, i de tre nästa är det två. Inom processen kan trådar köras parallellt – två HR-ansvariga tar emot två eller flera grupper av framtida anställda. Personalhandläggarnas interaktion med grupper - bearbetningen av flöden inom processen - kallas trådsynkronisering. Ritningarna av designen av två grupper av en personalofficer visar metoder: uniform (flicka - pojke - flicka - pojke) och med olika prioriteringar (två flickor alternerar med en pojke). Trådar har tillgång till resurserna i processen som de tillhör: grupper till HR-ansvarig får prov på ansökningsformulär, pennor för att fylla i dokument. Men om flöden interagerar med saker som är gemensamma för dem är incidenter möjliga. Om personaltjänstemannen ber dig att ropa namnet på den sista personen i kön, då är han, när det gäller två grupper, inte säker på förhand om han kommer att höra en kvinnas eller en mans namn. Sådana dataåtkomstkonflikter, blockering och sätt att lösa dem är ett mycket viktigt ämne.

Flödestillstånd

Varje tråd är i ett av följande tillstånd:
  • Skapad (Ny) – linjen för HR-ansvarig håller på att bli klar, folk blir organiserade.
  • Körbar – vår kö har ställts upp för HR-ansvarig och håller på att behandlas.
  • Blockerad – den sista killen i kön försöker skrika ut ett namn, men när han hörde att en tjej i nästa grupp började göra det före honom tystnade han.
  • Avslutad - hela kön är klar av HR-handläggaren och det finns inget behov av det.
  • Väntar – en kö väntar på en signal från en annan.
Organiseringen av trådar och deras interaktion är grunden för effektiv drift av processer.

Låt oss återvända till IT-världen

Under 2000-talet har flertrådigt och parallellt utförande blivit aktuellt. Sedan 90-talet av förra seklet har multitasking-operativsystemen Windows, MacOS och Linux blivit fast etablerade på hemdatorer. Du kan ofta hitta fyra eller fler kärnprocessorer i dem. Antalet parallella block av GPU-grafikkort har redan överskridit tusen. Populära program är skrivna med hänsyn till multithreading (multithreading), till exempel moderna versioner av programvara för att bearbeta grafik, video eller arbeta med stora mängder data: Adobe Photoshop, WinRar, Mathematica, moderna spel. Java multithreading är ett mycket viktigt, populärt och komplext ämne. Därför finns det många uppgifter i JavaRush-kursen för att förstå det mycket väl. Java-exempel på multithreading hjälper dig att bemästra de grundläggande nyanserna och subtiliteterna i detta område och synkronisera trådarnas arbete.

Bearbeta

Bearbeta(process) – en körande instans av ett program till vilket operativsystemet (OS) har allokerat minne, processortid/kärnor och andra resurser. Det är viktigt att minnet tilldelas separat. Adressutrymmena för olika processer är inte tillgängliga för varandra. Om processer behöver kommunicera kan de göra det med hjälp av filer, pipes och andra kommunikationsmetoder mellan processer.

Flöde

Java-tråd (tråd). Ibland, för att inte förväxlas med andra Java-klasser - Stream och liknande, översätts ofta Java-strömmar som en tråd. De använder de resurser som tilldelats en process och är det sätt som processen exekveras. Huvudtråden kör huvudmetoden och avslutar. När en process körs kan ytterligare trådar (undertrådar) skapas. Trådar i samma process kan utbyta data med varandra. Java multithreading kräver att datasynkronisering beaktas, glöm inte det. I Java avslutas en process när dess sista tråd har avslutats. För bakgrundsuppgifter kan en tråd startas som en demon, vilket skiljer sig från den vanliga genom att de kommer att tvångsavbrytas när alla icke-demonstrådar i processen avslutas.

Första flertrådiga applikationen

Det finns mer än ett halvdussin sätt att skapa trådar i JavaRush-kursen vi kommer att undersöka dem i detalj. Låt oss först bekanta oss med en av de grundläggande. Det finns en speciell klass Thread i run()-metoden där du behöver skriva kod som implementerar programlogiken. När du har skapat en tråd kan du starta den genom att anropa start()-metoden. Låt oss skriva ett demoprogram som implementerar ett exempel på Java multithreading. klass PeopleQueue utökar tråden ( // Vår kö av anställda, en ättling till trådklassen privata strängnamn; PeopleQueue(String...names) ( // Konstruktör, argument - array av anställdas namn detta. namn = namn; ) @Override public void run () ( // Denna metod kommer att anropas när tråden startar för (int i = 0 ; i< names. length; i++ ) { // Mata ut nästa medarbetare i en loop med en paus på 0,5 sekunder Systemet. ut. println( "Bearbetade dokument: "+ namn[i] ); försök (sömn (500); // Fördröjning 0,5 sek) fångst (Undantag e) ( ) ) ) ) offentlig klass HR ( // Klass för att visa hur tråden fungerar public static void main (String args) ( // Skapa två köer PeopleQueue queue1 = ny PeopleQueue ("Ivan", "Sergey", "Nikolai", "Ferdinand", "Vasily"); PeopleQueue queue2 = ny PeopleQueue ("Maria", "Lyudmila", "Alice", "Karina", "Olga"); Systemet. ut. println("Starta!"); // Meddelande från programmets huvudtråd kö1. Start(); //Starta en kö (underordnad tråd) kö2. Start(); //Starta den andra (undertråden)) ) Låt oss starta programmet. Konsolen visar meddelandet från huvudtråden. Därefter matar varje underordnad tråd kö1 och kö2 omväxlande ut meddelanden till sin gemensamma konsol om nästa bearbetade anställd. Ett av de möjliga alternativen för programmet: Låt oss börja! Bearbetade dokument: Maria Bearbetade handlingar: Ivan Bearbetade handlingar: Lyudmila Bearbetade handlingar: Sergey Bearbetade handlingar: Alisa Bearbetade handlingar: Nikolay Bearbetade handlingar: Karina Bearbetade handlingar: Ferdinand Bearbetade handlingar: Olga Bearbetade dokument: Vasily Processen avslutad med exitkod 0 Multithreading i Java– Ämnet är svårt och mångfacetterat. Möjligheten att skriva kod med hjälp av parallell, multitasking och multithreaded computing hjälper dig att effektivt implementera uppgifter på moderna flerkärniga processorer och kluster som består av många datorer. ska använda klassen java.lang.Thread. Den här klassen definierar alla metoder som behövs för att skapa trådar, hantera deras tillstånd och synkronisera dem.

Hur använder man klassen Tråd?

Det finns två möjligheter.

  • Först kan du skapa din egen barnklass baserat på trådklassen. När du gör det måste du åsidosätta körmetoden. Din implementering av denna metod kommer att köras i en separat tråd.
  • För det andra kan din klass implementera Runnable-gränssnittet. I det här fallet måste du inom din klass definiera en körmetod som fungerar som en separat tråd.

Den andra metoden är särskilt praktisk i fall där din klass måste ärvas från någon annan klass (till exempel från Applet-klassen) och samtidigt behöver du multithreading. Eftersom programmeringsspråket Java inte har flera arv, är det inte möjligt att skapa en klass som har Applet och Thread som sina överordnade klasser. I det här fallet är implementering av Runnable-gränssnittet det enda sättet att lösa problemet.

Trådklassmetoder

Klassen Thread definierar tre fält, flera konstruktörer och ett stort antal metoder utformade för att arbeta med trådar. Nedan har vi gett en kort beskrivning av fält, konstruktörer och metoder.

Med konstruktörer kan du skapa trådar på olika sätt, valfritt ange ett namn och en grupp för dem. Namnet är avsett att identifiera tråden och är ett valfritt attribut. När det gäller grupper är de utformade för att organisera skyddet av trådar från varandra inom samma applikation.

Metoder i klassen Thread tillhandahåller alla nödvändiga funktioner för att hantera trådar, inklusive synkronisering av dem.

Fält

Tre statiska fält används för att tilldela prioriteter till trådar.

  • NORM_PRIORITY

Vanligt

offentlig slutlig statisk int NORM_PRIORITY;
  • MAX_PRIORITY

Maximal

offentlig slutlig statisk int MAX_PRIORITY;
  • MIN_PRIORITY

Minimum

offentlig slutlig statisk int MIN_PRIORITY;

Konstruktörer

Skapar ett nytt trådobjekt

offentlig tråd();

Skapa ett nytt Thread-objekt som anger objektet som körmetoden ska anropas på

offentlig tråd (körbart mål); offentlig tråd (körbart mål, strängnamn);

Skapa ett trådobjekt genom att ange dess namn

offentlig tråd (strängnamn);

Skapa ett nytt trådobjekt som anger trådgruppen och objektet som körmetoden ska anropas på

offentlig tråd (trådgruppsgrupp, körbart mål);

Liknar den föregående, men dessutom anges namnet på det nya trådobjektet

offentlig tråd(Trådgruppsgrupp, Körbart mål, Strängnamn);

Skapa ett nytt Trådobjekt som anger trådgruppen och objektnamnet

offentlig tråd(Trådgruppsgrupp, strängnamn);

Metoder

  • activeCount

Det aktuella antalet aktiva trådar i gruppen som tråden tillhör

public static int activeCount();
  • checkAccess

Den aktuella tråden tillåts modifiera trådobjektet

public void checkAccesss();
  • countStackFrames

Bestämma antalet ramar på stapeln

public int countStackFrames();
  • aktuell tråd

Fastställer den aktuella tråden

public static Thread currentThread();
  • förstöra

Tvinga avslutning av en tråd

public void förstör();
  • dumpStack

Skriver ut det aktuella innehållet i stacken för felsökning

public static void dumpStack();
  • räkna upp

Få alla slitbaneobjekt i en given grupp

public static int enumerate(Thread tarray);
  • hämta namn

Bestämma trådnamnet

public final String getName();
  • getPriority

Fastställande av aktuell prioritet för en tråd

public final int getPriority();
  • getThreadGroup

Bestämma gruppen som en tråd tillhör

public final ThreadGroup getThreadGroup();
  • avbryta

Avbryter en tråd

public void interrupt();
  • avbröts
public static boolean interrupted();
  • lever

Avgör om en tråd körs eller inte

public final boolean isAlive();
  • är Daemon

Avgör om en tråd är en demon

public final boolean isDaemon();
  • är avbruten

Avgör om en tråd är avbruten

public boolean isInterrupted();
  • Ansluta sig

Väntar på att tråden ska slutföras

public final void join();

Vänta tills en tråd är klar under en viss tid. Tiden anges i millisekunder

public final void join(long millis);

Vänta tills en tråd är klar under en viss tid. Tiden anges i millisekunder och nanosekunder

public final void join(long millis, int nanos);
  • återuppta

Startar en tillfälligt avstängd tråd

public final void resume();

Metoden anropas om tråden skapades som ett objekt med Runnable-gränssnittet

public void run();
  • setDaemon

Ställer in tråden i demonläge

public final void setDaemon(boolean on);
  • Ange namn

Ställer in strömmens namn

public final void setName(String name);
  • prioritera

Ställa in trådprioritet

public final void setPriority(int newPriority);
  • sova
offentlig statisk tomrumssömn (lång millis);

Fördröja flödet under en viss tid. Tiden anges i millisekunder och nanosekunder

offentlig statisk tomrumssömn (lång millis, int nanos);
  • Start

Startar en tråd för utförande

public void start();
  • sluta

Stoppar en tråd från att köras

public final void stop();

Avbryt körningen av en tråd med ett specificerat undantag

offentligt slutgiltigt void stopp (Skastbart objekt);
  • uppskjuta

Pausar en tråd

public final void suspend();
  • att stränga

En sträng som representerar ett trådobjekt

public String toString();
  • avkastning

Pausar den aktuella tråden så att kontrollen kan överföras till en annan tråd

public static void yield();

Skapa en barnklass baserad på trådklassen

Låt oss överväga det första sättet att implementera multithreading, baserat på arv från klassen Thread. När du använder den här metoden definierar du en separat klass för tråden, till exempel så här:

class DrawRectangles förlänger tråden ( . . . public void run() ( . . . ) )

Detta definierar klassen DrawRectangles, som är ett barn i klassen Thread.

Var uppmärksam på körmetoden. När du skapar din klass baserat på klassen Thread måste du alltid definiera denna metod, som kommer att köras i en separat tråd.

Observera att körningsmetoden inte anropas direkt av några andra metoder. Den får kontroll när tråden startas med startmetoden.

Hur går det till?

Låt oss överväga proceduren för att starta en tråd med exemplet på en viss DrawRectangles-klass.

Först måste din applikation skapa ett objekt av klassen Thread:

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

Skapandet av objektet utförs av den nya operatören i startmetoden, som tar kontroll när användaren öppnar HTML-dokumentet som innehåller appleten. Omedelbart efter skapandet startas tråden för exekvering, för vilken startmetoden anropas.

När det gäller körmetoden, om en tråd används för att utföra en del periodiskt arbete, innehåller denna metod en oändlig slinga inuti sig själv. När slingan slutar och körmetoden återvänder, avslutas tråden på ett normalt, icke-nödlägessätt. För att avsluta en tråd onormalt kan du använda avbrottsmetoden.

Att stoppa en löpande tråd görs med stoppmetoden. Vanligtvis görs att stoppa alla löpande trådar som skapats av en applet med stoppmetoden för appletklassen:

public void stop() ( if (m_DrawRectThread != null) ( m_DrawRectThread.stop(); m_DrawRectThread = null; ) )

Kom ihåg att denna metod anropas när användaren lämnar webbserversidan som innehåller appleten.

Implementering av Runnable-gränssnittet

Metoden som beskrivs ovan för att skapa trådar som objekt av klassen Thread eller klasser som ärvs från den verkar ganska naturlig. Denna metod är dock inte den enda. Om du bara behöver skapa en tråd som körs samtidigt med appletkoden, är det lättare att välja den andra metoden med hjälp av Runnable-gränssnittet.

Tanken är att huvudappletklassen, som är ett barn till Applet-klassen, dessutom implementerar Runnable-gränssnittet, som visas nedan:

public class MultiTask utökar Applet implementerar Runnable ( Thread m_MultiTask = null; . . . public void run() ( . . ) public void start() ( if (m_MultiTask == null) ( m_MultiTask = new Thread(this); m_MultiTask. (); ) ) public void stop() ( if (m_MultiTask != null) ( m_MultiTask.stop(); m_MultiTask = null; ) )

Inuti klassen måste du definiera en körmetod som kommer att köras i en separat tråd. I det här fallet kan vi anse att appletkoden och körmetodens kod fungerar samtidigt som olika trådar.

Den nya operatorn används för att skapa en tråd. En tråd skapas som ett objekt i klassen Thread, och en referens till appletklassen skickas till konstruktorn:

m_MultiTask = ny tråd(detta);

I det här fallet, när tråden startar, kommer kontroll att tas emot av körmetoden som definieras i appletklassen.

Hur startar man en stream?

Lanseringen utförs som tidigare med startmetoden. Vanligtvis startas en tråd från appletens startmetod när användaren visar webbserversidan som innehåller appleten. Att stoppa en tråd görs med stoppmetoden.

Hallå! I den här artikeln kommer jag kort att introducera dig till processer, trådar och grunderna för flertrådsprogrammering i Java.
Den mest uppenbara tillämpningen av multithreading är gränssnittsprogrammering. Multithreading är viktigt när du vill att det grafiska användargränssnittet ska fortsätta att svara på användarinmatning medan viss bearbetning görs. Till exempel kan tråden som är ansvarig för gränssnittet vänta på att en annan tråd har laddat ner en fil från Internet och vid det här laget visa någon animering eller uppdatera förloppsindikatorn. Dessutom kan det stoppa tråden att ladda ner en fil om "avbryt"-knappen klickades.

Ett annat populärt och kanske ett av de mest hårda tillämpningsområdena för multithreading är spel. I spel kan olika trådar ansvara för arbete med nätverk, animation, fysikberäkningar m.m.

Låt oss börja. Först om processerna.

Processer

En process är en samling kod och data som delar ett gemensamt virtuellt adressutrymme. Oftast består ett program av en process, men det finns undantag (exempelvis skapar webbläsaren Chrome en separat process för varje flik, vilket ger den vissa fördelar, som att flikar är oberoende av varandra). Processer är isolerade från varandra, så direkt tillgång till minnet av en annan process är omöjlig (interaktion mellan processer utförs med speciella medel).

För varje process skapar operativsystemet ett så kallat "virtuellt adressutrymme" som processen har direkt åtkomst till. Detta utrymme tillhör processen, innehåller endast dess data och står helt till dess förfogande. Operativsystemet ansvarar för hur processens virtuella utrymme mappas till fysiskt minne.

Diagrammet för denna interaktion visas på bilden. Operativsystemet fungerar på så kallade minnessidor, som helt enkelt är ett område med en viss fast storlek. Om en process tar slut på minne allokerar systemet ytterligare sidor från det fysiska minnet till det. Virtuella minnessidor kan mappas till fysiskt minne i valfri ordning.

När ett program startar skapar operativsystemet en process, laddar programmets kod och data till dess adressutrymme och startar sedan huvudtråden för den skapade processen.

Strömmar

En tråd är en enhet för kodexekvering. Varje tråd exekverar sekventiellt instruktionerna för den process som den tillhör, parallellt med andra trådar i den processen.

Frasen "parallellt med andra trådar" bör diskuteras separat. Det är känt att det finns en exekveringsenhet per processorkärna vid varje given tidpunkt. Det vill säga, en enkärnig processor kan bara bearbeta kommandon sekventiellt, ett i taget (i ett förenklat fall). Men att köra flera parallella trådar är också möjligt på system med enkärniga processorer. I det här fallet kommer systemet periodvis att växla mellan trådar och växelvis tillåta den ena eller den andra tråden att köras. Detta schema kallas pseudo-parallelism. Systemet kommer ihåg tillståndet (kontexten) för varje tråd innan det byter till en annan tråd, och återställer den när tråden återgår till körning. Trådkontexten inkluderar parametrar som stacken, en uppsättning processorregistervärden, adressen till kommandot som exekveras, etc....

Enkelt uttryckt, med pseudo-parallell trådexekvering, rusar processorn mellan att exekvera flera trådar och exekverar en del av var och en av dem i tur och ordning.

Så här ser det ut:

De färgade rutorna i figuren är processorinstruktioner (gröna är instruktioner för huvudtråden, blå är instruktioner för en sidled). Utförandet går från vänster till höger. När en sidtråd väl har startat börjar dess instruktioner att utföras varvat med instruktionerna för huvudtråden. Antalet instruktioner som utförs för varje tillvägagångssätt är inte definierat.

Det faktum att instruktioner från parallella trådar exekveras blandat kan leda till dataåtkomstkonflikter i vissa fall. Nästa artikel kommer att ägnas åt problemen med trådinteraktion, men för nu ska vi prata om hur trådar lanseras i Java...

Löpande trådar

Varje process har minst en löpande tråd. Tråden från vilken programkörningen börjar kallas huvudtråden. I Java, efter att en process har skapats, börjar exekveringen av huvudtråden med metoden main(). Sedan, vid behov, på platser som specificeras av programmeraren, och när de villkor som anges av honom är uppfyllda, startas andra sidotrådar.

I Java representeras en tråd som ett underordnat objekt av klassen Thread. Denna klass kapslar in standardgängmekanismer.

Det finns två sätt att starta en ny tråd:

Metod 1
Skapa ett objekt av klassen Thread, skicka det i konstruktorn något som implementerar Runnable-gränssnittet. Det här gränssnittet innehåller metoden run() som kommer att köras i en ny tråd. En tråd kommer att slutföras när dess run()-metod är klar.

Det ser ut så här:

Klass SomeThing //Något som implementerar gränssnittet Runnable implements Runnable //(som innehåller metoden run() ( public void run() //Denna metod kommer att köras i en sidled ( System.out.println("Hej från side thread!") ; ) ) public class Program //Klass med metoden main() ( static SomeThing mThing; //mThing är ett objekt i en klass som implementerar det körbara gränssnittet public static void main(String args) ( mThing = new SomeThready(); ;

För att förkorta koden ytterligare kan du skicka ett objekt av en icke namngiven inre klass som implementerar Runnable-gränssnittet till konstruktören av klassen Thread:

Public class Program //Klass med metoden main(). ( public static void main(String args) ( //Skapa en tråd Thread myThready = new Thread(new Runnable() ( public void run() //Denna metod kommer att köras i en sidtråd ( System.out.println("Hej från sidan av tråden) tråd!"); ) )); myThready.start(); //Starta en tråd System.out.println("Huvudtråden har slutförts..."); ) )

Metod 2
Skapa en avkomling av klassen Thread och åsidosätt dess run()-metod:

Class AffableThread utökar tråden ( @Override public void run() //Denna metod kommer att exekveras i en sidled ( System.out.println("Hej från sidan tråden!"); ) ) public class Program ( statisk AffableThread mSecondThread; public static void main(String args) ( mSecondThread = new AffableThread(); //Skapa en tråd mSecondThread.start(); //Starta en tråd System.out.println("Huvudtråden har slutat..."); ) )

I exemplet ovan skapas en annan tråd och startas i metoden main(). Det är viktigt att notera att efter att ha anropat metoden mSecondThread.start() fortsätter huvudtråden att köras utan att vänta på att tråden den skapade ska slutföras. Och de instruktioner som kommer efter anrop av start()-metoden kommer att exekveras parallellt med instruktionerna i mSecondThread-tråden.

För att demonstrera den parallella driften av trådar, låt oss överväga ett program där två trådar argumenterar om den filosofiska frågan "vilken kom först, ägget eller hönan?" Huvudtråden är säker på att kycklingen kom först, vilket den kommer att rapportera varje sekund. Den andra tråden kommer att motbevisa sin motståndare en gång i sekunden. Tvisten kommer att pågå i 5 sekunder totalt. Vinnaren blir den ström som är den sista att yttra sitt svar på denna, utan tvekan, brännande filosofiska fråga. Exemplet använder funktioner som ännu inte har diskuterats (isAlive() sleep() och join()). Kommentarer ges till dem och de kommer att diskuteras mer i detalj.

Klass EggVoice utökar 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 exemplet ovan matar två trådar ut information till konsolen parallellt inom 5 sekunder. Det är omöjligt att förutsäga exakt vilken tråd som kommer att avslutas sist. Du kan prova, och du kan till och med gissa, men det finns en stor sannolikhet att samma program kommer att ha en annan "vinnare" nästa gång det körs. Detta händer på grund av något som kallas "asynkron kodexekvering". Asynkroni betyder att det inte kan sägas att någon instruktion för en tråd kommer att exekveras tidigare eller senare än instruktionen från en annan. Eller, med andra ord, parallella trådar är oberoende av varandra, förutom i de fall där programmeraren själv beskriver beroenden mellan trådar med hjälp av de språkverktyg som tillhandahålls för detta.

Nu lite om att slutföra processer...

Processavslutning och demoner

I Java avslutas en process när dess sista tråd avslutas. Även om main()-metoden redan har slutförts, men trådarna den skapade fortfarande körs, kommer systemet att vänta på att de ska slutföras.

Denna regel gäller dock inte för en speciell typ av tråd - demoner. Om den sista normala tråden i en process har slutförts och bara demontrådar finns kvar, kommer de att tvångsavslutas och processen avslutas. Oftast används demontrådar för att utföra bakgrundsuppgifter som tjänar en process under dess livstid.

Att förklara en tråd som en demon är ganska enkelt - du måste anropa dess metod innan du startar tråden setDaemon(true);
Du kan kontrollera om en tråd är en demon genom att anropa dess metod boolesk ärDaemon();

Avslutande trådar

Det finns (fanns) sätt i Java att tvinga en tråd att avslutas. Framför allt avslutar metoden Thread.stop() tråden omedelbart efter körning. Den här metoden, såväl som Thread.suspend(), som avbryter en tråd, och Thread.resume(), som fortsätter att köra en tråd, har föråldrats och deras användning är nu starkt avrådig. Faktum är att en tråd kan "dödas" när en operation utförs, vars avbrott i mitten av meningen kommer att lämna något objekt i fel tillstånd, vilket kommer att leda till uppkomsten av ett svårt att fånga och slumpmässigt uppträdande fel.

Istället för att tvinga en tråd att avslutas används ett schema där varje tråd är ansvarig för sin egen avslutning. En tråd kan stoppas antingen när den är klar med att köra run()-metoden (main() för huvudtråden) eller genom en signal från en annan tråd. Dessutom, hur man reagerar på en sådan signal är återigen en fråga för själva flödet. Efter att ha mottagit den kan tråden utföra vissa operationer och slutföra körningen, eller så kan den helt ignorera den och fortsätta körningen. Beskrivningen av reaktionen på trådavslutningssignalen vilar på programmerarens axlar.

Java har en inbyggd trådaviseringsmekanism som heter Interruption, som vi ska titta på snart, men först ta en titt på följande kod:

Incremenator är en tråd som lägger till eller subtraherar en från värdet på den statiska variabeln Program.mValue varje sekund. Incremenator innehåller två privata fält - mIsIncrement och mFinish. Vilken åtgärd som utförs bestäms av den booleska variabeln mIsIncrement - om det är sant, så görs tillägget av en, annars utförs subtraktionen. Och tråden slutar när mFinish-värdet blir sant.

Class Incremenator utökar tråden ( //Om det flyktiga nyckelordet - strax under private volatile boolean mIsIncrement = true; private volatile boolean mFinish = false; public void changeAction() //Ändrar åtgärden till det motsatta ( mIsIncrement = !mIsIncrement; ) public void finish( ) //Initierar slutförandet av tråden ( mFinish = true; ) @Override public void run() ( do ( if(!mFinish) //Kontrollerar behovet av att avsluta ( if(mIsIncrement) Program.mValue++; / /Increment else Program.mValue- -; //Decrement //Output det aktuella värdet av variabeln System.out.print(Program.mValue + " " else return // Avsluta tråden try( Thread.sleep(1000). ); // Pausa tråden i 1 sekund )catch(InterruptedException e)() ) while(true); //Side thread object public static void main(String args ) ( mInc = new Incremenator(); //Creating a thread System.out.print("Value = "); mInc.start(); //Starta tråden //Ändra inkrementeringsåtgärden tre gånger //med ett intervall på i*2 sekunder för(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 interagera med flödet med metoden changeAction() (för att ändra subtraktion till addition och vice versa) och metoden finish() (för att avsluta flödet).

Nyckelordet volatile användes i deklarationen av variablerna mIsIncrement och mFinish. Den måste användas för variabler som används av olika trådar. Detta beror på att värdet på en icke-flyktig variabel kan cachelagras separat för varje tråd, och värdet från den cachen kan vara olika för varje tråd. Att deklarera en variabel med det flyktiga nyckelordet inaktiverar sådan cachning för den och alla förfrågningar till variabeln kommer att skickas direkt till minnet.

Det här exemplet visar hur du kan organisera kommunikation mellan trådar. Det finns dock ett problem med detta tillvägagångssätt för att avsluta en tråd - Incremenator kontrollerar värdet på mFinish-fältet en gång per sekund, så det kan gå upp till en sekund mellan när finish()-metoden exekveras och tråden faktiskt slut. Det skulle vara bra om sleep()-metoden, när man tar emot en signal utifrån, returnerade exekvering och tråden omedelbart började avslutas. För att hantera detta scenario finns det ett inbyggt trådmeddelandeverktyg som heter Avbrott.

Avbrott

Trådklassen innehåller ett dolt booleskt fält, liknande fältet mFinish i Incremenator-programmet, kallat avbrottsflaggan. Denna flagga kan ställas in genom att anropa trådens interrupt()-metod. Det finns två sätt att kontrollera om denna flagga är inställd. Det första sättet är att anropa metoden bool isInterrupted() för thread-objektet, det andra är att anropa den statiska bool Thread.interrupted()-metoden. Den första metoden returnerar tillståndet för avbrottsflaggan och lämnar flaggan orörd. Den andra metoden returnerar flaggans tillstånd och återställer den. Observera att Thread.interrupted() är en statisk metod av klassen Thread, och dess anrop returnerar värdet på avbrottsflaggan för den tråd som den anropades från. Därför anropas denna metod endast inifrån en tråd och låter tråden kontrollera dess avbrottsstatus.

Så låt oss gå tillbaka till vårt program. Avbrottsmekanismen gör att vi kan lösa problemet med att tråden somnar. Metoder som avbryter exekveringen av en tråd, såsom sleep(), wait() och join(), har en egenhet - om trådens interrupt()-metod anropas medan de körs, kommer de att kasta ett InterruptedException utan att vänta på slutet av timeouten.

Låt oss omarbeta Incremenator-programmet - nu, istället för att avsluta tråden med finish()-metoden, kommer vi att använda standardmetoden interrupt()-metoden. Och istället för att kontrollera mFinish-flaggan kallar vi metoden bool Thread.interrupted();
Så här kommer Incremenator-klassen att se ut efter att ha lagt till avbrottsstöd:

Class Incremenator utökar tråden ( privat volatile boolean mIsIncrement = true; public void changeAction() //Reverserar åtgärden ( mIsIncrement = !mIsIncrement; ) @Override public void run() ( do ( if(!Thread.interrupted()) / / Avbryt kontroll ( if(mIsIncrement) Program.mValue++; //Increment else Program.mValue--; //Decrement //Output det aktuella värdet för variabeln System.out.print(Program.mValue + " "); ) else return / / /Avsluta tråden try( Thread.sleep(1000); //Stäng av tråden i 1 sekund. )catch(InterruptedException e)( return; //Avsluta tråden efter avbrott ) ) while(true) ) class Program ( //Variable); out.print("Value = " ); mInc.start(); i<= 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 blev vi av med finish()-metoden och implementerade samma trådavslutningsmekanism med det inbyggda avbrottssystemet. I den här implementeringen har vi en fördel - metoden sleep() kommer att returnera kontroll (kasta ett undantag) direkt efter att tråden har avbrutits.

Observera att metoderna sleep() och join() är inslagna i try-catch-konstruktioner. Detta är en nödvändig förutsättning för att dessa metoder ska fungera. Koden som anropar dem måste fånga InterruptedException som de kastar när de avbryts medan de väntar.

Vi har sorterat ut start- och sluttrådar, sedan ska jag prata om metoderna som används när man arbetar med trådar.

Thread.sleep()-metoden

Thread.sleep() är en statisk metod av klassen Thread som avbryter exekveringen av tråden som den anropades på. Medan sleep()-metoden körs slutar systemet att allokera processortid till tråden och distribuera den bland andra trådar. Sleep()-metoden kan köras antingen under en viss tid (millisekunder eller nanosekunder) eller tills den stoppas av ett avbrott (i vilket fall det kommer att kasta ett InterruptedException).

Thread.sleep(1500); //Väntar en och en halv sekund Thread.sleep(2000, 100); //Väntar 2 sekunder och 100 nanosekunder

Även om metoden sleep() kan ta nanosekunder att vänta, bör den inte tas lätt på. I många system är väntetiden fortfarande avrundad till millisekunder eller till och med tiotals.

yield() metod

Den statiska metoden Thread.yield() får processorn att byta till att bearbeta andra trådar i systemet. Metoden kan vara användbar, till exempel när en tråd väntar på att en händelse ska inträffa och det är nödvändigt att kontrollera om det inträffar så ofta som möjligt. I det här fallet kan du lägga händelsekontrollen och metoden Thread.yield() i en loop:

//Väntar på att ett meddelande ska anlända while(!msgQueue.hasMessages()) //Medan det inte finns några meddelanden i kön ( Thread.yield(); //Överför kontrollen till andra trådar)

join() metod

Java tillhandahåller en mekanism för att låta en tråd vänta på att en annan tråd slutförs. Metoden join() används för detta. Till exempel, för att få huvudtråden att vänta på att sidtråden myThready ska slutföras, måste du utfärda myThready.join()-satsen på huvudtråden. När myThready avslutas, returnerar join()-metoden och huvudtråden kan fortsätta att köras.

Metoden join() har en överbelastning som tar en timeout som parameter. I det här fallet returnerar join() antingen när tråden den väntar på avslutas eller när timeouten går ut. Precis som metoden Thread.sleep() kan joinmetoden vänta i millisekunder och nanosekunder - argumenten är desamma.

Genom att ställa in en tråds timeout kan du till exempel uppdatera en animerad bild medan huvudtråden (eller någon annan) väntar på att en sidtråd som utför resurskrävande operationer ska slutföra:

Thinker brain = new Thinker(); //Thinker är en ättling till trådklassen. brain.start(); //Börja "tänka". do ( mThinkIndicator.refresh(); //mThinkIndicator - animerad bild. try( brain.join(250); //Vänta en kvarts sekund tills tanken slutar. ) catch(InterruptedException e)() ) while(brain .lever()); //Medan hjärnan tänker... //hjärnan har tänkt färdigt (det applåderas).

I det här exemplet tänker hjärntråden på något och det ska ta lång tid att göra det. Huvudtråden väntar på honom i en kvarts sekund och, om den här tiden för att tänka inte räcker, uppdaterar "tankeindikatorn" (någon animerad bild). Som ett resultat, medan han tänker, observerar användaren en indikator på tankeprocessen på skärmen, vilket låter honom veta att de elektroniska hjärnorna är upptagna med något.

Trådprioriteringar

Varje tråd i systemet har sin egen prioritet. Prioritet är något nummer i trådobjektet, vars högre värde betyder högre prioritet. Systemet exekverar trådar med högre prioritet först, och trådar med lägre prioritet får CPU-tid endast när deras mer privilegierade motsvarigheter är inaktiva.

Du kan arbeta med trådprioriteringar med två funktioner:

void setPriority(int priority)– ställer in prioritet för tråden.
Möjliga prioritetsvärden är MIN_PRIORITY, NORM_PRIORITY och MAX_PRIORITY.

int getPriority()– får trådprioritet.

Några användbara metoder i klassen Thread

Det är praktiskt taget allt. Slutligen, här är några användbara metoder för att arbeta med strömmar.

boolean isAlive()- returnerar true om myThready() körs och false om tråden ännu inte har startats eller har avslutats.

setName(String threadName)– Anger namnet på strömmen.
Sträng getName()– Får trådens namn.
Trådnamnet är en sträng som är associerad med det, vilket i vissa fall hjälper till att förstå vilken tråd som utför en åtgärd. Ibland kan detta vara användbart.

statisk tråd Thread.currentThread()- en statisk metod som returnerar objektet i tråden där det anropades.

långt getId()– returnerar trådidentifieraren. Identifierare är ett unikt nummer som tilldelas strömmen.

Slutsats

Jag noterar att artikeln inte talar om alla nyanser av flertrådsprogrammering. Och koden som ges i exemplen saknar några nyanser för att vara helt korrekt. I synnerhet använder exemplen inte synkronisering. Trådsynkronisering är ett ämne utan vilket du inte kommer att kunna programmera ordentliga flertrådiga applikationer. Du kan läsa om det till exempel i boken "Java Concurrency in Practice" eller