In Java kunt u toepassingen met meerdere threads maken. Thread en Runnable, welke moet je kiezen? Wat is multithreading?

Het schrijven van parallelle code is geen gemakkelijke taak, en het controleren van de juistheid ervan is nog moeilijker. Ondanks dat Java uitgebreide ondersteuning biedt voor multithreading en synchronisatie op taal- en API-niveau, blijkt in de praktijk dat het schrijven van correcte multithreaded Java-code afhangt van de ervaring en toewijding van de individuele programmeur. Hieronder vindt u een aantal tips om u te helpen uw multi-threaded Java-code naar een hoger niveau te tillen. Sommigen van jullie zijn misschien al bekend met deze tips, maar het kan nooit kwaad om ze eens in de paar jaar op te frissen.

Veel van deze tips kwamen van leren en praktisch programmeren en ook na het lezen van de boeken "Java concurrency in de praktijk" en "Effective Java". Ik raad elke Java-programmeur aan om de eerste twee keer te lezen; ja, dat klopt, TWEE KEER. Gelijktijdigheid is een verwarrend en moeilijk te begrijpen onderwerp (zoals bijvoorbeeld voor sommigen - recursie), en na één keer lezen, begrijp je misschien niet alles helemaal.

Het enige doel van het gebruik van concurrency is om schaalbare en snelle toepassingen, maar er moet altijd aan worden herinnerd dat snelheid geen belemmering mag zijn voor correctheid. Uw Java-programma moet voldoen aan zijn invariant, ongeacht of het in single-threaded of multi-threaded vorm is gestart. Als je nieuw bent in parallel programmeren, om te beginnen, check out verschillende problemen ontstaan ​​bij parallelle lancering programma's (bijvoorbeeld: impasse, raceconditie, honger naar hulpbronnen, enz.).

1. Gebruik lokale variabelen

Probeer altijd lokale variabelen te gebruiken in plaats van klasse of statische velden. Soms gebruiken ontwikkelaars klassenvelden om geheugen te besparen en variabelen opnieuw te gebruiken, ervan uitgaande dat het nodig is om een ​​lokale variabele voor elke methodeaanroep te maken: een groot aantal extra geheugen... Een voorbeeld van een dergelijk gebruik is een verzameling, die wordt gedeclareerd als een statisch veld en opnieuw kan worden gebruikt met de methode clear (). Dit plaatst de klasse in een algemene toestand, die het niet zou moeten hebben, aangezien is oorspronkelijk gemaakt voor parallel gebruik op meerdere threads. In de onderstaande code wordt de methode execute () aangeroepen vanuit verschillende threads en was een tijdelijke verzameling vereist om de nieuwe functionaliteit te implementeren. V originele code er werd een statische verzameling (List) gebruikt en de bedoeling van de ontwikkelaar was duidelijk - om de verzameling aan het einde van de methode execute () te wissen, zodat deze later opnieuw kan worden gebruikt. De ontwikkelaar geloofde dat zijn code thread-safe was omdat de CopyOnWriteArrayList thread-safe is. Maar dit is niet het geval - de methode execute () wordt aangeroepen vanuit verschillende threads en een van de threads heeft toegang tot de gegevens die door de andere thread zijn geschreven om gemeenschappelijke lijst... Synchronisatie geleverd door CopyOnWriteArrayList in in dit geval onvoldoende om de invariantie van de methode execute () te garanderen.

Public static class ConcurrentTask (private static List temp = Collections.synchronizedList (new ArrayList ()); @Override public void execute (Message message) (// Gebruik de lokale tijdelijke lijst // List temp = new ArrayList (); // Toevoegen om iets op te sommen van het bericht temp.add ("message.getId ()"); temp.add ("message.getCode ()"); temp.clear (); // kan nu opnieuw worden gebruikt))

Probleem: De gegevens van het ene bericht komen in het andere terecht als twee oproepen () "overlappen", dwz. de eerste thread zal de id van het eerste bericht toevoegen, daarna zal de tweede thread de id van het tweede bericht toevoegen (dit gebeurt zelfs voordat de lijst is gewist), waardoor de gegevens van een van de berichten beschadigd raken.

Oplossingsopties:

  1. Voeg een synchronisatieblok toe aan het deel van de code waar de thread iets toevoegt aan de tijdelijke lijst en het wist. Een andere thread heeft dus geen toegang tot de lijst totdat de eerste ermee klaar is. In dit geval is dit deel van de code single-threaded, wat de prestaties van de applicatie als geheel zal verminderen.
  2. Gebruik een lokale lijst in plaats van het klasseveld. Ja, het zal de geheugenkosten verhogen, maar het zal het synchronisatieblok verwijderen en de code leesbaarder maken. Ook over tijdelijke objecten hoef je je geen zorgen te maken - de vuilnisman zorgt voor ze.

Slechts een van de gevallen wordt hier gepresenteerd, maar bij het schrijven van parallelle code geef ik persoonlijk de voorkeur aan lokaal variabele velden class, als dit laatste niet vereist is door de applicatiearchitectuur.

2. Geef de voorkeur aan onveranderlijke klassen boven veranderlijk

De meest algemeen erkende praktijk in multithreaded Java-programmering is het gebruik van onveranderlijke (onveranderlijk) klassen. Onveranderlijke klassen zoals String, Integer en andere maken het gemakkelijker om parallelle code in Java te schrijven, omdat: u hoeft zich geen zorgen te maken over de staat van de objecten van deze klassen. Onveranderlijke klassen verminderen het aantal synchronisatie-items in uw code. Een object van een onveranderlijke klasse, eenmaal gemaakt, kan niet worden gewijzigd. Het beste voorbeeld van zo'n klasse is de string (java.lang.String). Elke bewerking om een ​​tekenreeks in Java te wijzigen (conversie naar hoofdletters, een subtekenreeks nemen, enz.) zal resulteren in de creatie van een nieuw String-object voor het resultaat van de bewerking, waarbij de oorspronkelijke tekenreeks ongewijzigd blijft.

3. Verklein de synchronisatiegebieden

Elke code binnen het synchronisatiedomein kan niet parallel worden uitgevoerd, en als in uw programma 5% van de code zich in synchronisatieblokken bevindt, dan kan volgens de wet van Amdahl de prestatie van de gehele applicatie niet meer dan 20 keer worden verbeterd. belangrijkste reden Dit komt omdat 5% van de code altijd sequentieel wordt uitgevoerd. U kunt deze hoeveelheid verminderen door de synchronisatiegebieden te verkleinen - probeer ze alleen voor kritieke secties te gebruiken. Beste voorbeeld Beperking van synchronisatiebereik - dubbel gecontroleerde vergrendeling, die kan worden geïmplementeerd in Java 1.5 en hoger met behulp van vluchtige variabelen.

4. Gebruik een threadpool

Stream maken (Draad)- een dure operatie. Als u een schaalbare Java-toepassing wilt bouwen, moet u een threadpool gebruiken. Naast de omslachtige aard van de aanmaakbewerking, genereert handmatige stroomregeling veel repetitieve code die, wanneer gemengd met bedrijfslogica, de algehele leesbaarheid van de code vermindert. Flow control is de taak van een framework, of het nu een Java-tool is of wat je maar wilt gebruiken. De JDK heeft een goed georganiseerd, uitgebreid en volledig getest framework dat bekend staat als het Executor-framework dat overal kan worden gebruikt waar een threadpool nodig is.

5. Gebruik synchronisatieprogramma's in plaats van wachten () en informeren ()

Java 1.5 introduceerde veel hulpprogramma's voor synchronisatie, zoals CyclicBarrier, CountDownLatch en Semaphore. U moet altijd eerst leren wat de JDK heeft voor synchronisatie voordat u wait () en notificatie () gebruikt. Het zal veel gemakkelijker zijn om het reader-writer-patroon te implementeren met BlockingQueue dan met wait () en notificeren (). Het zal ook veel gemakkelijker zijn om te wachten op 5 threads om de berekening te voltooien met CountDownLatch dan om hetzelfde te implementeren via wait () en notificeren (). Bekijk het pakket java.util.concurrent om gelijktijdige code naar te schrijven: Java de beste manier.

6. Gebruik BlockingQueue om Producer-Consumer te implementeren

Dit advies volgt uit het vorige, maar ik heb het apart belicht vanwege het belang ervan voor parallelle toepassingen gebruikt in de echte wereld. De oplossing voor veel multithreading-problemen is gebaseerd op het Producer-Consumer-patroon en BlockingQueue - De beste manier implementeren in Java. In tegenstelling tot Exchanger, dat kan worden gebruikt in het geval van een enkele schrijver en lezer, kan BlockingQueue worden gebruikt om: correcte verwerking verschillende schrijvers en lezers.

7. Gebruik thread-safe collecties in plaats van collecties te blokkeren

Thread-safe collecties bieden grotere schaalbaarheid en prestaties dan hun blokkerende tegenhangers (Collections.synchronizedCollection, enz.). СoncurrentHashMap, wat naar mijn mening de meest populaire thread-safe collectie is, laat betere prestaties zien dan het blokkeren van HashMap of Hashtable wanneer het aantal lezers groter is dan het aantal schrijvers. Een ander voordeel van thread-safe collecties is dat ze zijn geïmplementeerd met een nieuw vergrendelingsmechanisme (java.util.concurrent.locks.Lock) en de native synchronisatiemechanismen gebruiken die worden geleverd door de onderliggende hardware en JVM. Gebruik daarnaast CopyOnWriteArrayList in plaats van Collections.synchronizedList als u vaker uit de lijst leest dan deze wijzigt.

8. Gebruik semaforen om beperkingen te creëren

Om een ​​betrouwbaar en stabiel systeem te creëren, moet u resourcebeperkingen hebben (databases, bestandssysteem, stopcontacten, enz.). Je code mag nooit een oneindige hoeveelheid bronnen creëren en/of gebruiken. Semaphores (java.util.concurrent.Semaphore) - een goede keuze om beperkingen op te leggen aan het gebruik van dure bronnen, zoals databaseverbindingen (in dit geval kunt u trouwens een verbindingspool gebruiken). Semaforen helpen beperkingen te creëren en threads te blokkeren als een bron niet beschikbaar is.

9. Gebruik synchronisatieblokken in plaats van blokkeermethoden

Deze tip breidt uit op de tip voor het verkleinen van synchronisatiegebieden. Het gebruik van synchronisatieblokken is een methode om het synchronisatiebereik te verkleinen, waardoor u ook een ander object kunt vergrendelen dan het object dat momenteel wordt weergegeven door de aanwijzer deze. De eerste kandidaat moet een atomaire variabele zijn en vervolgens een vluchtige variabele als ze voldoen aan uw synchronisatievereisten. Als je wederzijdse uitsluiting nodig hebt, gebruik dan eerst ReentrantLock of een gesynchroniseerd blok. Als u nog niet bekend bent met gelijktijdig programmeren en geen vitale belangrijke toepassing, kunt u gewoon een gesynchroniseerd blok gebruiken, wat veiliger en gemakkelijker is.

10. Vermijd het gebruik van statische variabelen

Zoals blijkt uit de eerste tip, kunnen statische variabelen, wanneer ze in parallelle code worden gebruikt, tot veel problemen leiden. Als u een statische variabele gebruikt, zorg er dan voor dat het een constante of een onveranderlijke verzameling is. Als je overweegt de collectie opnieuw te gebruiken om geheugen te besparen, ga dan terug naar de eerste tip.

11. Gebruik Lock in plaats van gesynchroniseerd

Deze laatste bonustip moet met de nodige voorzichtigheid worden gebruikt. Interface vergrendelen - krachtig gereedschap maar zijn kracht brengt ook een grote verantwoordelijkheid met zich mee. Diverse objecten Vergrendelingen op lees- en schrijfbewerkingen stellen u in staat schaalbare gegevensstructuren zoals ConcurrentHashMap te implementeren, maar vereisen grote zorg bij uw programmering. In tegenstelling tot een gesynchroniseerd blok, geeft een thread het slot niet automatisch vrij. U moet expliciet unlock () aanroepen om te ontgrendelen. Het is een goede gewoonte om deze methode in een definitief blok aan te roepen, zodat het blok onder alle voorwaarden eindigt:

Slot.slot (); probeer (// doe iets ...) eindelijk (lock.unlock ();)

Gevolgtrekking

Je kreeg tips voor het schrijven van multi-threaded code in Java. Nogmaals, het kan nooit kwaad om "Java-concurrency in de praktijk" en "Effectieve Java" van tijd tot tijd opnieuw te lezen. Je kunt ook de manier van denken ontwikkelen die je nodig hebt voor parallel programmeren, gewoon door de code van iemand anders te lezen en problemen tijdens de ontwikkeling te visualiseren. Vraag uzelf ten slotte af welke regels u volgt bij het ontwikkelen van multithreaded Java-applicaties?

In theoretische werken over multithreading kun je beschrijvingen vinden van drie taken die, volgens de auteurs, alle mogelijke multithreading-taken omvatten: de producent-consument-taak, de lezer-schrijver-taak, de eetfilosofen-taak. De allegorie is mooi en best goed op zijn eigen manier, maar naar mijn mening zegt het voor een onvolwassen jonge programmeur helemaal niets. Daarom zal ik de problemen met mijn klokkentoren beschrijven. Er zijn slechts twee problemen.

Het eerste probleem is toegang tot één bron uit verschillende threads. We hebben het probleem met één schop al beschreven. Je kunt de optie uitbreiden - er is één tank water (met één kraan), 25 dorstige mijnwerkers en 5 mokken voor iedereen. We zullen moeten onderhandelen, anders kan de moord beginnen. Bovendien is het niet alleen nodig om de mokken intact te houden - het is ook noodzakelijk om alles zo te organiseren dat iedereen kan drinken. Dit gaat gedeeltelijk naar probleem nummer twee.
Het tweede probleem is de synchronisatie van interactie. Op de een of andere manier kreeg ik een taak aangeboden - schrijven eenvoudig programma om twee streams te laten pingpongen. De ene schrijft "Ping" en de andere schrijft "Pong". Maar ze moeten het op hun beurt doen. Laten we ons nu voorstellen dat we dezelfde taak moeten doen, maar voor 4 threads - we spelen een paar voor een paar.

Die. de probleemstelling is heel eenvoudig. Eenmalig - u moet de toegang tot de gedeelde bron ordelijk en veilig organiseren. Twee - u moet de threads in een bepaalde volgorde uitvoeren.
Het is aan de uitvoering. En hier wachten ons veel moeilijkheden, waarover ze met een ademteug spreken (en misschien niet tevergeefs). Laten we beginnen met een gedeelde bron.

Gedeelde bron voor meerdere threads

Ik stel voor om het probleem onmiddellijk aan te tonen met een eenvoudig voorbeeld. Zijn taak is om 200 klassenthreads te starten CounterThread... Elke stream krijgt een link naar een enkel object Balie... Tijdens de uitvoering roept de thread de methode van dit object aan vermeerderTeller duizend keer. Methode verhoogt een variabele balie door 1. Nadat we 200 threads hebben gelanceerd, wachten we tot ze eindigen (we vallen gewoon 1 seconde in slaap - dit is voldoende). En aan het eind printen we het resultaat. Kijk naar de code - naar mijn mening is alles daar vrij transparant:

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

openbare klasse CounterTester

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

ct. begin ();

Draad. slaap (1000);

klasse Teller

privé lange teller = 0L;

openbare leegte verhogingCounter () (

teller ++;

openbare lange getCounter () (

retour teller;

privé Teller teller;

deze. teller = teller;

@ Overschrijven

openbare ongeldige uitvoering () (

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

Logischerwijs zouden we het volgende resultaat moeten krijgen - 200 threads van 1000 toevoegingen = 200000. Maar verschrikking, dit is helemaal niet het geval. Mijn resultaten variëren, maar duidelijk niet 200.000. Wat is het probleem? Het probleem is dat we vanuit 200 threads tegelijkertijd proberen de methode aan te roepen vermeerderTeller... Op het eerste gezicht gebeurt er niets verschrikkelijks - we voegen gewoon toe aan de variabele balie eenheid. Wat is daar zo verschrikkelijk aan?
Het vreselijke is dat de schijnbaar onschuldige code om er een toe te voegen eigenlijk niet in één stap wordt uitgevoerd. Eerst lezen we de waarde van de variabele in een register, dan voegen we er een aan toe, dan schrijven we het resultaat terug naar de variabele. Zoals je kunt zien, zijn er meer dan één stappen (in het geheim - er zijn zelfs meer dan drie stappen die ik heb beschreven). En stel je nu voor dat twee threads (of zelfs meer) tegelijkertijd de waarde van de variabele lezen - er was bijvoorbeeld een waarde van 99. Nu tellen beide threads één op bij 99, krijgen beide 100 en schrijven beide deze waarde naar de variabele. Wat gebeurt daar? Het is gemakkelijk te zien dat het er 100 zullen zijn. En het zouden er 101 moeten zijn. Het zou nog erger kunnen zijn als een of andere thread "erin slaagde" 98 te tellen en "vast kwam te zitten" in de rij met threads voor uitvoering. Dan halen we nog geen 100. trekhaak

Toegang tot gedeelde bronnen is een van de grootste problemen bij multithreading. Omdat ze erg sluw is. Je kunt alles heel betrouwbaar doen, maar dan gaan de prestaties achteruit. En zodra je "speling" geeft (opzettelijk, voor productiviteit), zal er zeker een situatie ontstaan ​​dat de "speling" in al zijn glorie naar buiten komt.

Het toverwoord - gesynchroniseerd

Wat kan er gedaan worden om van de situatie af te komen waarin we ons bevinden met onze prachtige stromen. Laten we beginnen met een kleine speculatie. Als we naar de winkel komen, gaan we naar de kassa om te betalen. De kassier bedient slechts één persoon tegelijk. We staan ​​allemaal in de rij voor haar. In feite wordt de kassa een exclusief hulpmiddel dat door slechts één klant tegelijk kan worden gebruikt. Multithreading biedt precies dezelfde manier - u kunt een bepaalde bron definiëren als exclusief voor slechts één thread tegelijk. Zo'n hulpmiddel wordt een "monitor" genoemd. Dit is het meest voorkomende object dat een thread moet "vangen". Alle threads die toegang willen tot deze monitor (object) worden in de wachtrij geplaatst. En hiervoor hoeft u geen speciale code te schrijven - u hoeft alleen maar te proberen de monitor te "vangen". Maar hoe definieer je dit? Laten we het uitzoeken.
Ik stel voor om ons voorbeeld uit te voeren, maar met één extra woord in de methodebeschrijving vermeerderTeller- dit woord gesynchroniseerd.

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

pakket edu. javacursus. balie;

openbare klasse CounterTester

public static void main (String args) gooit InterruptedException (

Tellerteller = nieuwe Teller ();

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

CounterThread ct = nieuwe CounterThread (teller);

ct. begin ();

Draad. slaap (1000);

Systeem. uit. println ("Teller:" + teller. getCounter ());

klasse Teller

privé lange teller = 0L;

openbare gesynchroniseerde ongeldige verhogingCounter () (

teller ++;

openbare lange getCounter () (

retour teller;

klasse CounterThread breidt Thread uit

privé Teller teller;

openbare CounterThread (tellerteller) (

deze. teller = teller;

@ Overschrijven

openbare ongeldige uitvoering () (

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

balie. vermeerderTeller ();

En ... kijk eens aan. Alles werkte. We krijgen het verwachte resultaat - 200000. Wat doet dit toverwoord - gesynchroniseerd ?
Woord gesynchroniseerd zegt dat voordat een thread deze methode op ons object kan aanroepen, het ons object moet "vangen" en vervolgens de vereiste methode moet uitvoeren. Nogmaals en voorzichtig (soms wordt een iets andere benadering voorgesteld, die naar mijn mening buitengewoon gevaarlijk en onjuist is - ik zal het iets later beschrijven) - eerst "vangt" de draad (locks - van het woord lock - lock, naar block) het monitor-object (in ons geval is het klasse-object Balie) en pas daarna kan de thread de methode uitvoeren vermeerderTeller... Exclusief, helemaal alleen zonder concurrenten.
Er is een andere interpretatie gesynchroniseerd wat misleidend kan zijn - het klinkt ongeveer als volgt: meerdere threads kunnen niet tegelijkertijd een gesynchroniseerde methode invoeren. Dit is niet waar. Want dan blijkt dat als een klasse meerdere methoden heeft gesynchroniseerd, dan kunnen twee verschillende methoden van hetzelfde object tegelijkertijd worden uitgevoerd, gemarkeerd als gesynchroniseerd... Dit is niet waar. Als een klasse 2, 3 of meer methoden heeft gesynchroniseerd, en wanneer er ten minste één wordt uitgevoerd, wordt het hele object vergrendeld. Dit betekent dat alle methoden die zijn aangeduid als gesynchroniseerd zijn niet beschikbaar voor andere streams. Als de methode niet als zodanig is gelabeld. dat is geen probleem - doe het voor uw gezondheid.
En nogmaals - eerst hebben ze "gevangen", daarna hebben ze de methode uitgevoerd, daarna "vrijgelaten". Nu is het object vrij en degene die het als eerste uit de stromen heeft gevangen, heeft gelijk.
In het geval dat de methode wordt gedeclareerd als statisch, dan wordt de hele klasse het monitorobject en wordt de toegang ertoe geblokkeerd op het niveau van alle objecten van deze klasse.

Bij de bespreking van het artikel wezen ze me op de onjuistheid, die ik (voor de eenvoud) bewust heb toegegeven, maar het is waarschijnlijk logisch om het te vermelden. Het gaat om de methode getCounter... Strikt genomen moet het ook worden aangeduid als: gesynchroniseerd omdat op het moment dat onze variabele verandert, een andere thread deze wil lezen. En om problemen te voorkomen, moet de toegang tot deze variabele in alle methoden worden gesynchroniseerd.
hoewel wat betreft getCounter, dan kun je hier een nog interessantere functie gebruiken - de atomiciteit van operaties. Je leest erover in het artikel Atoomtoegang. De hoofdgedachte is dat het lezen en schrijven van enkele elementaire typen en referenties in één stap gebeurt en in principe veilig is. Als het veld balie was bijvoorbeeld int, dan zou het mogelijk zijn om een ​​niet-synchrone methode in te lezen. Voor type: lang en dubbele we moeten een variabele declareren balie hoe vluchtig... Waarom dit misschien merkwaardig is - er moet rekening mee worden gehouden dat: int bestaat uit 4 bytes en je kunt je de situatie voorstellen dat het nummer niet in één stap wordt geschreven. Maar dit is puur theoretisch - de JVM garandeert ons dat het lezen en schrijven van een elementair int-type in één stap wordt gedaan en geen enkele thread zal in staat zijn om in deze operatie te wringen en iets te bederven.

Er is een andere manier om het woord te gebruiken gesynchroniseerd- niet in de beschrijving van de methode, maar in de code. Laten we ons voorbeeld nogmaals veranderen in het methodegedeelte vermeerderTeller.

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

pakket edu. javacursus. balie;

openbare klasse CounterTester

public static void main (String args) gooit InterruptedException (

Tellerteller = nieuwe Teller ();

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

Laten we, voordat we meer te weten komen over Java-streams, eens kijken naar de nabije toekomst. Stel je voor dat je een cv hebt aangevraagd en bent geïnterviewd. Jij en een paar dozijn toekomstige collega's werden uitgenodigd om bij een groot softwarebedrijf te werken. U moet onder meer papieren documenten voor tewerkstelling indienen bij een vermoeide HR-medewerker.

Om het proces te versnellen, kunnen sollicitanten voor de functie in twee groepen worden verdeeld en worden verdeeld over twee HR-managers (indien aanwezig in het bedrijf). Als resultaat krijgen we de versnelling van het proces vanwege de parallel ( parallel) werken aan de registratie.

Als er maar één personeelsfunctionaris in het bedrijf is, moet je op de een of andere manier eruit zien te komen. Je kunt bijvoorbeeld iedereen weer in twee groepen verdelen, bijvoorbeeld afwisselend meisjes en jongens interviewen.

Of volgens een ander principe: aangezien er meer mensen in de onderste groep zitten, wisselen we twee meisjes af voor één jongen.

Deze manier van werk organiseren heet meerdradig... Onze vermoeide HR-medewerker schakelt over naar verschillende groepen om er een andere medewerker van te maken. Er kunnen elf groepen zijn en vier personeelsfunctionarissen. In dit geval multithreaded ( multithreading) zal de verwerking parallel plaatsvinden door meerdere HR-s, die de volgende persoon uit een groep kunnen nemen om zijn documenten te verwerken.

Processen

Het proces ( werkwijze) in dit geval zal er de organisatie zijn van het ontvangen van documenten. In een organisatie kunnen verschillende processen worden onderscheiden: boekhouding, softwareontwikkeling, ontmoetingen met klanten, magazijnwerking, enz. Voor elk proces worden middelen toegewezen: lokalen, medewerkers voor de uitvoering ervan. Processen staan ​​los van elkaar: HR-medewerkers hebben geen toegang tot de boekhouddatabase en accountmanagers rennen niet door het magazijn. Als een proces toegang nodig heeft tot andermans bronnen, is het noodzakelijk om communicatie tussen processen tot stand te brengen: memo's, gezamenlijke vergaderingen.

Streams

Werk in het proces is georganiseerd in de vorm van threads (java-thread). Flow is voor de HR-afdeling de organisatie van het werk voor het onderhoud van de groep. De allereerste foto toont één stream, de volgende drie - twee. Binnen het proces kunnen threads parallel worden uitgevoerd - twee HR-functionarissen nemen twee of meer groepen toekomstige werknemers aan. De interactie van HR-functionarissen met groepen - verwerkingsstromen binnen het proces - heet gesynchroniseerde streams... In de tekeningen van het ontwerp van één personeelsfunctionaris van twee groepen zijn werkwijzen zichtbaar: uniform (meisje - jongen - meisje - jongen) en met verschillende prioriteiten (twee meisjes wisselen elkaar af met één jongen). Threads hebben toegang tot de bronnen van het proces waartoe ze behoren: groepen van de HR-functionaris krijgen voorbeelden van aanvraagformulieren, pennen voor het invullen van documenten. Maar als de threads interageren met dingen die ze gemeen hebben, zijn incidenten mogelijk. Als een personeelsfunctionaris vraagt ​​om de naam van de laatste persoon in de rij te roepen, dan weet hij bij twee groepen vooraf niet zeker of hij een vrouwelijke of een mannelijke naam zal horen. Dergelijke conflicten met betrekking tot gegevenstoegang, vergrendelingen en hoe deze op te lossen, zijn een zeer belangrijk onderwerp.

Streamstatussen

Elke thread bevindt zich in een van de volgende statussen:
  • Gemaakt (Nieuw) - de wachtrij voor de personeelsfunctionaris wordt voorbereid, mensen worden georganiseerd.
  • Runnable - onze wachtrij staat in de rij voor de HR-functionaris en wordt verwerkt.
  • Geblokkeerd - de jonge man die als laatste in de rij staat, probeert een naam te roepen, maar toen hij hoorde dat het meisje in de naburige groep het voor hem begon te doen, viel hij stil.
  • Beëindigd - de hele wachtrij is ingevuld door de personeelsfunctionaris en is niet nodig.
  • Wachten - de ene wachtrij wacht op een signaal van een andere.
De organisatie van stromen en hun interactie is de basis voor een efficiënte werking van processen.

Terug naar de IT-wereld

In de 21e eeuw zijn multithreading en parallelle uitvoering relevant geworden. Sinds de jaren 90 van de vorige eeuw zijn multitasking-besturingssystemen Windows, macOS en Linux stevig ingeburgerd in thuiscomputers. Ze bevatten vaak vier of meer kernprocessors. Het aantal parallelle blokken GPU-videokaarten heeft de duizend al overschreden. Populaire programma's zijn geschreven rekening houdend met multithreading, bijvoorbeeld moderne versies van software voor het verwerken van afbeeldingen, video of het werken met grote hoeveelheden gegevens: Adobe Photoshop, WinRar, Mathematica, moderne games. Java multithreading is een zeer belangrijk, veelgevraagd en complex onderwerp. Daarom zijn er veel taken in de CodeGym-cursus om er heel goed mee om te gaan. Java-voorbeelden over multithreading helpen u de basisnuances en subtiliteiten van dit gebied, threadsynchronisatie, onder de knie te krijgen.

Werkwijze

Werkwijze(proces) is een actief exemplaar van een programma waaraan het besturingssysteem (OS) geheugen, processortijd/cores en andere bronnen heeft toegewezen. Het is belangrijk dat het geheugen apart wordt toegewezen, de adresruimten van verschillende processen zijn niet voor elkaar toegankelijk. Als processen moeten communiceren, kunnen ze dat doen met behulp van bestanden, leidingen en andere communicatiemethoden tussen processen.

Stromen

Java-thread (thread). Soms, om verwarring met andere Java-Stream-klassen en dergelijke te voorkomen, worden Java-streams vaak vertaald als een thread. Ze gebruiken de middelen die aan het proces zijn toegewezen en zijn de manier waarop het proces wordt uitgevoerd. De hoofdthread voert de hoofdmethode uit en sluit af. Tijdens de uitvoering van het proces kunnen extra threads (child) worden voortgebracht. Threads van één proces kunnen gegevens met elkaar uitwisselen. Java multithreading vereist dat er rekening wordt gehouden met gegevenssynchronisatie, vergeet dat niet. In Java eindigt een proces wanneer de laatste thread is voltooid. Voor achtergrondtaken kan de thread worden gestart als een daemon, die verschilt van de gebruikelijke doordat ze geforceerd worden beëindigd wanneer alle niet-daemon-threads van het proces worden beëindigd.

Eerste multithreaded applicatie

Er zijn meer dan een half dozijn manieren om streams te maken, en we zullen ze in detail behandelen in de CodeGym-cursus. Laten we eerst kennis maken met een van de basisprincipes. Er is een speciale klasse Thread in de methode run () waarvan je code moet schrijven die de logica van het programma implementeert. Nadat u een thread hebt gemaakt, kunt u deze starten door de methode start () aan te roepen. Laten we een demoprogramma schrijven dat een voorbeeld van Java-multithreading implementeert. klasse PeopleQueue breidt Thread uit ( // Onze werknemerswachtrij, overgenomen van de Thread-klasse privé String-namen; PeopleQueue (String... Namen) ( // Constructor, argument is een reeks werknemersnamen deze. namen = namen; ) @Override public void run () ( // Deze methode wordt aangeroepen wanneer de thread start voor (int i = 0; i< names. length; i++ ) { // Uitvoer in een lus met een pauze van 0,5 seconden voor de volgende medewerker Systeem. uit. println ( "Documenten verwerkt:"+ namen [i]); probeer (slaap (500); // Vertraging 0,5 sec) vangst (Uitzondering e) ()))) openbare klasse HR ( // Klasse om te demonstreren hoe de thread werkt public static void main (String args) ( // Maak twee wachtrijen PeopleQueue wachtrij1 = nieuwe PeopleQueue ("Ivan", "Sergey", "Nikolay", "Ferdinand", "Vasily"); PeopleQueue-wachtrij2 = nieuwe PeopleQueue ("Maria", "Lyudmila", "Alice", "Karina", "Olga"); Systeem. uit. println ("Aan de slag!"); // Bericht uit de hoofdthread van het programma wachtrij1. begin (); // Start één wachtrij (onderliggende thread) wachtrij2. begin (); // Start de tweede (kindthread))) Laten we het programma starten. In de console kunt u de berichtuitvoer door de hoofdthread zien. Verder voeren elke onderliggende thread wachtrij1 en wachtrij2 afwisselend berichten uit naar de gemeenschappelijke console voor hen over de volgende verwerkte werknemer. Een van de mogelijke opties voor het programma: Start! Verwerkte documenten: Maria Verwerkte documenten: Ivan Verwerkte documenten: Lyudmila Verwerkte documenten: Sergey Verwerkte documenten: Alice Verwerkte documenten: Nikolay Verwerkte documenten: Karina Verwerkte documenten: Ferdinand Verwerkte documenten: Olga Verwerkte documenten: Vasily Verwerking afgewerkt met exit code 0 Multithreading in Java- het onderwerp is moeilijk en veelzijdig. Als u weet hoe u code moet schrijven met parallelle, multitasking en multithreaded computing, kunt u taken efficiënt implementeren op moderne multi-coreprocessors en clusters van veel computers. moet de klasse java.lang.Thread gebruiken. Deze klasse definieert alle methoden die nodig zijn om threads te maken, hun status te beheren en ze te synchroniseren.

Hoe gebruik ik de Thread-klasse?

Er zijn twee mogelijkheden.

  • Ten eerste kunt u uw eigen kindklasse maken op basis van de Thread-klasse. Wanneer u dit doet, moet u de run-methode overschrijven. Uw implementatie van deze methode zal op een aparte thread worden uitgevoerd.
  • Ten tweede kan uw klas de Runnable-interface implementeren. In dit geval moet je binnen je klas een run-methode definiëren die als een aparte thread zal werken.

De tweede methode is vooral handig in gevallen waarin je klasse moet erven van een andere klasse (bijvoorbeeld van de Applet-klasse) en je tegelijkertijd multithreading nodig hebt. Aangezien de Java-programmeertaal geen meervoudige overerving heeft, is het niet mogelijk om een ​​klasse te creëren die de klassen Applet en Thread als ouder dient. In dit geval is het implementeren van de Runnable-interface de enige manier om het probleem op te lossen.

Methoden voor draadklassen

De klasse Thread definieert drie velden, verschillende constructors en een groot aantal methoden om met threads te werken. Hieronder hebben we een korte beschrijving gegeven van velden, constructors en methoden.

Met constructors kunt u op verschillende manieren streams maken, en indien nodig een naam en groep opgeven. De naam is voor stream-identificatie en is een optioneel attribuut. Wat betreft de groepen, ze zijn ontworpen om de bescherming van threads van elkaar binnen dezelfde applicatie te organiseren.

De methoden van de klasse Thread bieden alle benodigde mogelijkheden voor het beheren van threads, inclusief het synchroniseren ervan.

Velden

Drie statische velden zijn voor het prioriteren van threads.

  • NORM_PRIORITY

normaal

openbare definitieve statische int NORM_PRIORITY;
  • MAX_PRIORITY

maximaal

openbare definitieve statische int MAX_PRIORITY;
  • MIN_PRIORITY

Minimum

openbare definitieve statische int MIN_PRIORITY;

Constructeurs

Een nieuw Thread-object maken

openbare discussie ();

Creëren van een nieuw Thread-object dat het object specificeert waarvoor de run-methode wordt aangeroepen

openbare thread (uitvoerbaar doel); openbare thread (uitvoerbaar doel, stringnaam);

Een Thread-object maken met zijn naam

openbare thread (tekenreeksnaam);

Maak een nieuw Thread-object met vermelding van de threadgroep en het object waarvoor de run-methode wordt aangeroepen

openbare thread (ThreadGroup-groep, uitvoerbaar doel);

Vergelijkbaar met de vorige, maar met extra vermelding van de naam van het nieuwe Thread-object

openbare thread (ThreadGroup-groep, uitvoerbaar doel, tekenreeksnaam);

Maak een nieuw Thread-object met vermelding van de threadgroep en de objectnaam

openbare thread (ThreadGroup-groep, Stringnaam);

Methoden:

  • activeCount

Het huidige aantal actieve threads in de groep waartoe de thread behoort

openbare statische int activeCount ();
  • checkToegang

De huidige thread mag het Thread-object wijzigen

public void checkAccesss ();
  • countStackFrames

Het aantal frames op de stapel bepalen

openbare int countStackFrames ();
  • huidigeThread

De huidige lopende thread bepalen

openbare statische Thread currentThread ();
  • kapot maken

Afsluiting van een draad forceren

openbare leegte vernietigen ();
  • dumpStack

De huidige inhoud van de stapel weergeven voor foutopsporing

openbare statische leegte dumpStack ();
  • opsommen

Alle profielen in een bepaalde groep ophalen

openbare statische int enumerate (Thread tarray);
  • getName

De naam van een stream bepalen

openbare finale String getName ();
  • prioriteit krijgen

De huidige prioriteit van een thread bepalen

openbare finale int getPriority ();
  • getThreadGroup

De groep bepalen waartoe de stream behoort

openbare finale ThreadGroup getThreadGroup ();
  • onderbreken

Een stream onderbreken

openbare leegte onderbreken ();
  • onderbroken
openbare statische boolean onderbroken ();
  • is levend

Bepalen of een thread loopt of niet

publieke finale boolean isAlive ();
  • isDaemon

Bepalen of een thread een daemon is

publieke finale boolean isDaemon ();
  • is onderbroken

Bepalen of een thread wordt onderbroken

public boolean isInterrupted ();
  • meedoen

Wachten tot een thread is voltooid

openbare definitieve void join ();

Wachten op het voltooien van een thread gedurende een bepaalde tijd. De tijd wordt ingesteld in milliseconden.

openbare finale leegte join (lange millis);

Wachten op het voltooien van een thread gedurende een bepaalde tijd. Tijd wordt ingesteld in milliseconden en nanoseconden

openbare finale leegte join (lange millis, int nanos);
  • CV

Een tijdelijk opgeschorte thread starten

openbare definitieve nietig hervatten ();

De methode wordt aangeroepen als de thread is gemaakt als een object met de Runnable-interface

openbare ongeldige run ();
  • setDaemon

Instelling voor een daemon-modusthread

openbare definitieve void setDaemon (boolean aan);
  • setName

Streamnaam instellen

openbare definitieve void setName (String-naam);
  • prioriteit instellen

Draadprioriteit instellen

openbare definitieve void setPriority (int newPriority);
  • slaap
openbare statische leegte slaap (lange millis);

Vertraging van de stroom voor de terug tijd. Tijd wordt ingesteld in milliseconden en nanoseconden

openbare statische leegte slaap (lange millis, int nanos);
  • begin

Een thread starten voor uitvoering

openbare ongeldige start ();
  • stoppen

Uitvoering van thread stoppen

openbare definitieve nietig stop ();

Abnormale beëindiging van thread-uitvoering met gespecificeerde uitzondering

openbare definitieve ongeldige stop (Gooibare obj);
  • opschorten

Een stream onderbreken

openbare definitieve nietig opschorten ();
  • toString

Een tekenreeks die het stream-object vertegenwoordigt

public String naarString ();
  • opbrengst

De huidige thread opschorten zodat de controle wordt overgedragen aan een andere thread

openbare statische leegte opbrengst ();

Een onderliggende klasse maken op basis van de klasse Thread

Laten we eens kijken naar de eerste manier om multithreading te implementeren, gebaseerd op overerven van de Thread-klasse. Als je deze methode gebruikt, definieer je een aparte klasse voor de stream, bijvoorbeeld als volgt:

klasse DrawRectangles breidt Thread uit (... public void run () (...))

Dit definieert de klasse DrawRectangles, die een kind is van de klasse Thread.

Besteed aandacht aan de run-methode. Wanneer u uw klasse maakt op basis van de klasse Thread, moet u altijd deze methode definiëren, die binnen een aparte thread wordt uitgevoerd.

Merk op dat de run-methode niet rechtstreeks door een andere methode wordt aangeroepen. Het krijgt controle wanneer de thread begint met de startmethode.

Hoe gebeurde dit?

Laten we eens kijken naar de procedure voor het starten van een thread met een DrawRectangles-klasse als voorbeeld.

Eerst moet uw toepassing een object van de klasse Thread maken:

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

Het maken van objecten wordt uitgevoerd door de nieuwe operator in de startmethode, die de controle overneemt wanneer de gebruiker een HTML-document opent met een applet. Direct na het maken wordt de thread gestart voor uitvoering, waarvoor de startmethode wordt aangeroepen.

Wat betreft de run-methode, als de thread wordt gebruikt om periodiek werk uit te voeren, bevat deze methode een oneindige lus erin. Wanneer de lus eindigt en de run-methode terugkeert, wordt de thread normaal afgesloten, niet abnormaal. U kunt de interrupt-methode gebruiken om een ​​thread abnormaal te beëindigen.

Het stoppen van een lopende thread wordt uitgevoerd met behulp van de stop-methode. Doorgaans wordt het stoppen van alle actieve threads die door een applet zijn gemaakt, gedaan door de stop-methode van de applet-klasse:

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

Bedenk dat deze methode wordt aangeroepen wanneer de gebruiker de webserverpagina verlaat die de applet bevat.

De uitvoerbare interface implementeren

De bovenstaande manier om threads te maken als objecten van de Thread-klasse of klassen die ervan zijn geërfd, lijkt heel natuurlijk. Deze methode is echter niet de enige. Als u slechts één thread hoeft te maken die tegelijkertijd met de appletcode wordt uitgevoerd, is het gemakkelijker om de tweede methode te kiezen met behulp van de Runnable-interface.

Het idee is dat de hoofdapplet-klasse, die een kind is van de Applet-klasse, bovendien de Runnable-interface implementeert, zoals hieronder wordt weergegeven:

public class MultiTask breidt Applet-implementaties uit Runnable (Thread m_MultiTask = null;... public void run () (...) public void start () (if (m_MultiTask == null) (m_MultiTask = new Thread (this); m_MultiTask. start ();)) public void stop () (if (m_MultiTask! = null) (m_MultiTask.stop (); m_MultiTask = null;)))

Binnen de klasse moet je een run-methode definiëren die in een aparte thread wordt uitgevoerd. In dit geval kunnen we aannemen dat de code van de applet en de code van de run-methode gelijktijdig werken als verschillende threads.

De nieuwe operator wordt gebruikt om een ​​stream te maken. Een thread wordt gemaakt als een object van de Thread-klasse en een verwijzing naar de applet-klasse wordt doorgegeven aan de constructor:

m_MultiTask = nieuwe thread (dit);

In dit geval, wanneer de thread start, wordt de besturing ontvangen door de run-methode die is gedefinieerd in de appletklasse.

Hoe start ik een draadje?

De start wordt, zoals eerder, uitgevoerd door de startmethode. Gewoonlijk wordt een thread gestart vanaf de startmethode van een applet wanneer de gebruiker de webserverpagina weergeeft die de applet bevat. Het stoppen van de stroom wordt gedaan door de stopmethode.

Hallo! In dit artikel geef ik je een kort overzicht van processen, threads en de basisprincipes van multithreaded programmeren in de Java-taal.
Het meest voor de hand liggende gebruik voor multithreading is bij het programmeren van interfaces. Multithreading is onmisbaar als u wilt dat de GUI blijft reageren op gebruikersinvoer terwijl er informatie wordt verwerkt. De thread die verantwoordelijk is voor de interface kan bijvoorbeeld wachten op de voltooiing van een andere thread die een bestand van internet downloadt, en op dat moment een animatie weergeven of de voortgangsbalk bijwerken. Bovendien kan het de thread stoppen met het laden van het bestand als de "annuleren"-knop werd ingedrukt.

Een ander populair en misschien wel een van de meest hardcore toepassingen van multithreading is gamen. In games kunnen verschillende threads verantwoordelijk zijn voor netwerken, animatie, natuurkundige berekeningen, enz.

Laten we beginnen. Ten eerste over de processen.

Processen

Een proces is een verzameling code en gegevens die een gemeenschappelijke virtuele adresruimte deelt. Meestal bestaat één programma uit één proces, maar er zijn uitzonderingen (de Chrome-browser maakt bijvoorbeeld een apart proces voor elk tabblad, wat het enkele voordelen geeft, zoals onafhankelijkheid van tabbladen van elkaar). Processen staan ​​los van elkaar, waardoor directe toegang tot het geheugen van andermans proces niet mogelijk is (interactie tussen processen gebeurt met speciale middelen).

Voor elk proces creëert het besturingssysteem een ​​zogenaamde "virtuele adresruimte" waartoe het proces directe toegang heeft. Deze ruimte hoort bij het proces, bevat alleen de gegevens en staat volledig ter beschikking. Het besturingssysteem is verantwoordelijk voor hoe de virtuele ruimte van het proces op het fysieke geheugen wordt geprojecteerd.

Het diagram van deze interactie wordt getoond in de afbeelding. Het besturingssysteem werkt op zogenaamde geheugenpagina's, die gewoon een gebied zijn van een bepaalde vaste grootte. Als een proces onvoldoende geheugen heeft, wijst het systeem er extra pagina's uit het fysieke geheugen aan toe. Virtuele geheugenpagina's kunnen in willekeurige volgorde aan het fysieke geheugen worden toegewezen.

Wanneer het programma start, creëert het besturingssysteem een ​​proces, laadt de programmacode en gegevens in de adresruimte en start vervolgens de hoofdthread van het gemaakte proces.

Streams

Eén thread is één eenheid van code-uitvoering. Elke thread voert opeenvolgend instructies uit van het proces waartoe het behoort, parallel met de andere threads in dat proces.

De zinsnede "parallel met andere stromen" moet apart worden besproken. Het is bekend dat er op elk moment één uitvoeringseenheid per processorkern is. Dat wil zeggen, een processor met één kern kan opdrachten alleen opeenvolgend verwerken, één voor één (in een vereenvoudigd geval). Het starten van meerdere parallelle threads is echter ook mogelijk op systemen met single-core processors. In dit geval zal het systeem periodiek schakelen tussen threads, waarbij afwisselend de ene of de andere thread kan worden uitgevoerd. Dit wordt pseudo-parallelisme genoemd. Het systeem onthoudt de status (context) van elke thread voordat naar een andere thread wordt geschakeld, en herstelt deze bij terugkeer naar de uitvoering van de thread. De threadcontext bevat parameters zoals de stapel, een set processorregisterwaarden, het adres van de opdracht die wordt uitgevoerd, enzovoort ...

Simpel gezegd, bij pseudo-parallelle uitvoering van threads, haast de processor zich tussen de uitvoering van verschillende threads en voert op zijn beurt een deel van elk van hen uit.

Zo ziet het eruit:

De gekleurde vierkanten in de afbeelding zijn processorinstructies (groen - instructies voor de hoofddraad, blauw - voor de zijdraad). Uitvoering gaat van links naar rechts. Nadat de zijstroom is gestart, beginnen de instructies te lopen, afgewisseld met de instructies van de hoofdthread. Het aantal instructies dat voor elke benadering moet worden uitgevoerd, is niet gedefinieerd.

Het vermengen van parallelle thread-instructies kan in sommige gevallen leiden tot gegevenstoegangsconflicten. Het volgende artikel zal gewijd zijn aan de problemen van thread-interactie, maar voor nu, over hoe threads worden gestart in Java ...

Streams starten

Elk proces heeft ten minste één lopende thread. De thread van waaruit de uitvoering van het programma begint, wordt de hoofdthread genoemd. In Java begint de uitvoering van de hoofdthread na het maken van een proces vanaf de methode main (). Vervolgens worden, indien nodig, op de door de programmeur gespecificeerde plaatsen en wanneer aan de door hem gespecificeerde voorwaarden is voldaan, andere zijstromen gelanceerd.

In Java wordt een thread weergegeven als een afstammeling van de klasse Thread. Deze klasse omvat standaard schroefdraadmechanismen.

Er zijn twee manieren om een ​​nieuwe thread te starten:

Methode 1
Maak een object van de klasse Thread door er iets aan door te geven in de constructor die de Runnable-interface implementeert. Deze interface bevat een methode run () die op een nieuwe thread wordt uitgevoerd. De thread wordt voltooid wanneer de methode run () is voltooid.

Het ziet er zo uit:

Class SomeThing // Iets dat de Runnable-interface implementeert, implementeert Runnable // (met een methode run ()) (public void run () // Deze methode wordt uitgevoerd op een zijstroom (System.out.println ("Hallo van een kant stream!") ;)) public class Program // Een klasse met een main () methode (static SomeThing mThing; // mThing is een object van een klasse die de uitvoerbare interface implementeert public static void main (String args) (mThing = new SomeThing (); Thread myThready = new Thread (mThing); // Maak thread "myThready" myThready.start (); // Start thread System.out.println ("Hoofdthread voltooid ...");))

Om de code verder in te korten, kunt u een object van een naamloze innerlijke klasse die de Runnable-interface implementeert, doorgeven aan de constructor van de Thread-klasse:

Openbare klasse Programma // Klasse met main () methode. (public static void main (String args) (// Thread maken Thread myThready = new Thread (new Runnable () (public void run () // Deze methode wordt uitgevoerd op een zijthread (System.out.println ("Hallo van side thread! ");))); myThready.start (); // Start de thread System.out.println (" De hoofdthread is klaar ... ");))

Methode 2
Maak een afstammeling van de klasse Thread en overschrijf de methode run () ervan:

Class AffableThread breidt Thread uit (@Override public void run () // Deze methode wordt uitgevoerd op een zijthread (System.out.println ("Hallo van een zijthread!");)) Public class Program (static AffableThread mSecondThread; public static void main (String args) (mSecondThread = new AffableThread (); // Maak een thread mSecondThread.start (); // Start thread System.out.println ("Hoofdthread voltooid ...");))

In het bovenstaande voorbeeld wordt een andere thread gemaakt en gestart in de methode main (). Het is belangrijk op te merken dat na het aanroepen van de methode mSecondThread.start () de hoofdthread doorgaat met de uitvoering zonder te wachten tot de thread die is voortgekomen, is voltooid. En de instructies die volgen op de aanroep van de methode start () worden parallel uitgevoerd met de instructies van de mSecondThread-thread.

Laten we, om te demonstreren hoe threads parallel werken, eens kijken naar een programma waarin twee threads discussiëren over de filosofische vraag "wat was er eerst, een ei of een kip?" De rode draad is dat de kip de eerste was, wat hij elke seconde zal melden. De tweede stroom, eens per seconde, zal zijn tegenstander weerleggen. Het totale geschil duurt 5 seconden. De stroom die als laatste zijn antwoord zal geven op deze ongetwijfeld brandende filosofische vraag, zal winnen. Het voorbeeld gebruikt functies die nog niet genoemd zijn (isAlive () sleep () en join ()). Er worden opmerkingen aan hen gegeven en ze zullen later in meer detail worden besproken.

Class EggVoice breidt Thread uit (@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("Спор закончен!"); } } Консоль: Спор начат... курица! яйцо! яйцо! курица! яйцо! курица! яйцо! курица! яйцо! курица! Первой появилась курица! Спор закончен!

In het gegeven voorbeeld voeren twee parallelle stromen gedurende 5 seconden informatie uit naar de console. Het is onmogelijk om nauwkeurig te voorspellen welke stream als laatste zal spreken. Je kunt het proberen en zelfs raden, maar de kans is groot dat hetzelfde programma de volgende keer dat het wordt gelanceerd een andere "winnaar" heeft. Dit komt door de zogenaamde "asynchrone code-uitvoering". Asynchronie betekent dat je niet kunt zeggen dat een instructie van de ene thread eerder of later wordt uitgevoerd dan instructies van een andere. Of, met andere woorden, parallelle threads zijn onafhankelijk van elkaar, behalve in die gevallen waarin de programmeur zelf de afhankelijkheden tussen threads beschrijft met behulp van de taalfaciliteiten die hiervoor beschikbaar zijn.

Nu een beetje over het beëindigen van processen ...

Procesbeëindiging en daemons

In Java eindigt een proces wanneer de laatste thread eindigt. Zelfs als de methode main () al is voltooid, maar de threads die het voortbrengt nog steeds worden uitgevoerd, wacht het systeem totdat ze zijn voltooid.

Deze regel is echter niet van toepassing op een speciaal soort thread - daemons. Als de laatste normale thread van het proces wordt afgesloten en alleen de daemon-threads overblijven, worden deze geforceerd beëindigd en wordt het proces beëindigd. Meestal worden daemon-threads gebruikt om achtergrondtaken uit te voeren die een proces tijdens zijn levensduur dienen.

Het declareren van een thread als een daemon is vrij eenvoudig - je moet de methode aanroepen voordat je de thread start setDaemon (waar);
U kunt controleren of een thread een daemon is door de methode ervan aan te roepen boolean isDaemon ();

Beëindiging van streams

Java heeft (bestaande) middelen om een ​​thread te dwingen te beëindigen. In het bijzonder beëindigt de methode Thread.stop () de thread onmiddellijk nadat deze is uitgevoerd. Deze methode, evenals Thread.suspend (), die de thread opschort, en Thread.resume (), die de uitvoering van de thread voortzet, zijn echter verouderd en het gebruik ervan wordt nu ten zeerste afgeraden. Het feit is dat de draad kan worden "gedood" tijdens de uitvoering van een bewerking, waarvan de onderbreking in het midden van een woord een object in de verkeerde staat zal achterlaten, wat zal leiden tot het verschijnen van een moeilijk te vangen en willekeurig optredende fout.

In plaats van geforceerde beëindiging van een thread, wordt een schema gebruikt waarin elke thread verantwoordelijk is voor zijn eigen beëindiging. De thread kan stoppen wanneer het klaar is met het uitvoeren van de methode run () (main () - voor de hoofdthread), of op een signaal van een andere thread. En hoe te reageren op zo'n signaal is, nogmaals, een kwestie van de stroom zelf. Als de thread het heeft ontvangen, kan het enkele bewerkingen uitvoeren en de uitvoering voltooien, of het kan het helemaal negeren en doorgaan met uitvoeren. Het is de verantwoordelijkheid van de programmeur om de reactie op het draadbeëindigingssignaal te beschrijven.

Java heeft een ingebouwd meldingsmechanisme voor threads, Interruption genaamd, dat we binnenkort zullen bespreken, maar kijk eerst eens naar het volgende programma:

Incremenator is een thread die elke seconde één optelt of aftrekt van de waarde van de statische variabele Program.mValue. De Incremenator bevat twee privévelden, mIsIncrement en mFinish. Welke actie wordt uitgevoerd, wordt bepaald door de booleaanse variabele mIsIncrement - als het waar is, wordt er één opgeteld, anders - aftrekken. En het einde van de stream vindt plaats wanneer de waarde van mFinish waar wordt.

Class Incremenator breidt thread uit (// Over het trefwoord vluchtig - net onder privé vluchtig boolean mIsIncrement = true; privé vluchtig boolean mFinish = false; public void changeAction () // Draai de actie om (mIsIncrement =! MIsIncrement;) public void finish ( ) // Start de beëindiging van de thread (mFinish = true;) @Override public void run () (do (if (! MFinish) // Check if (! MFinish) // Controleer of het moet worden beëindigd (if (mIsIncrement) Program.mValue ++; // Increment else Program.mValue- -; // Decrement // Print de huidige waarde van de variabele System.out.print (Program.mValue + "");) else return; // Beëindig de thread try ( Thread.sleep (1000); // Onderbreek de thread voor 1 sec. ) catch (InterruptedException e) ()) while (true);)) public class Programma (// Variabele beheerd door de incrementator public static int mValue = 0; static Incremenator mInc; // Object van de side stream public static void main (String args ) (mInc = new Incremenator (); // Create stream S ystem.out.print ("Waarde ="); mInc.start (); // Start de thread // Drievoudige wijziging van de incrementatoractie // met een interval van i * 2 seconden voor (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

U kunt met de stream communiceren met behulp van de methode changeAction () (om aftrekken te veranderen in optellen en vice versa) en de methode finish () (om de stream te beëindigen).

Het vluchtige sleutelwoord werd gebruikt in de declaraties van de mIsIncrement- en mFinish-variabelen. Het moet worden gebruikt voor variabelen die door verschillende threads worden gebruikt. Dit komt omdat de waarde van een variabele die zonder vluchtig is gedeclareerd, voor elke thread afzonderlijk in de cache kan worden opgeslagen, en de waarde van deze cache kan voor elke thread anders zijn. Het declareren van een variabele met het vluchtige sleutelwoord schakelt dergelijke caching uit en alle verzoeken aan de variabele worden direct naar het geheugen gestuurd.

Dit voorbeeld laat zien hoe u de communicatie tussen threads kunt organiseren. Er is echter één probleem met deze benadering van threadbeëindiging: de Incremenator controleert de waarde van het mFinish-veld eenmaal per seconde, dus het kan tot een seconde duren tussen het moment waarop de finish ()-methode wordt uitgevoerd en de daadwerkelijke beëindiging van de thread . Het zou geweldig zijn als, bij ontvangst van een signaal van buitenaf, de sleep ()-methode de uitvoering zou retourneren en de thread onmiddellijk zou beginnen met beëindigen. Om dit scenario te bereiken, is er een ingebouwde threadwaarschuwing met de naam Interruption.

Onderbreking

De klasse Thread bevat een verborgen Boolean-veld, vergelijkbaar met het veld mFinish in het programma Incremenator, de interrupt-vlag genoemd. Deze vlag kan worden ingesteld door de methode interrupt () van de thread aan te roepen. Er zijn twee manieren om te controleren of deze vlag is ingesteld. De eerste manier is om de bool isInterrupted () methode van het thread object aan te roepen, de tweede is om de bool statische methode Thread.interrupted () aan te roepen. De eerste methode retourneert de status van de interrupt-vlag en laat die vlag onaangeroerd. De tweede methode retourneert de status van de vlag en stelt deze opnieuw in. Merk op dat Thread.interrupted () een statische methode is van de klasse Thread en dat het aanroepen ervan de waarde van de interruptvlag van de thread teruggeeft van waaruit het werd aangeroepen. Daarom wordt deze methode alleen vanuit de thread aangeroepen en kan de thread zijn interruptstatus controleren.

Dus terug naar ons programma. Met het interruptmechanisme kunnen we het probleem oplossen waarbij de draad in slaap valt. Methoden die de uitvoering van een thread opschorten, zoals sleep (), wait () en join (), hebben één bijzonderheid: als tijdens hun uitvoering de methode interrupt () van deze thread wordt aangeroepen, zullen ze een InterruptedException genereren zonder te wachten op de time-out verloopt.

Laten we het programma Incremenator opnieuw maken - in plaats van de thread te beëindigen met de methode finish () zullen we de standaard methode interrupt () gebruiken. En in plaats van de mFinish-vlag te controleren, zullen we de bool-methode Thread.interrupted () aanroepen;
Dit is hoe de Incremenator-klasse eruit zal zien na het toevoegen van interrupt-ondersteuning:

Class Incremenator breidt Thread uit (private vluchtige boolean mIsIncrement = true; public void changeAction () // Keer de actie om (mIsIncrement =! MIsIncrement;) @Override public void run () (do (if (! Thread.interrupted ()) / / Interrupt check (if (mIsIncrement) Program.mValue ++; // Increment else Program.mValue--; // Decrement // Print de huidige waarde van de variabele System.out.print (Program.mValue + "");) else return; // Beëindig de thread try (Thread.sleep (1000); // Onderbreek de thread gedurende 1 sec.) Catch (InterruptedException e) (return; // Beëindig de thread na onderbreking)) while (true);) ) class Programma (// Variabele bediend door de incrementator public static int mValue = 0; static Incremenator mInc; // Object van de side stream public static void main (String args) (mInc = new Incremenator (); // Create stream System .out.print ("Value =" ); mInc.start (); // Start de thread // Drievoudige wijziging van de incrementatoractie // met een interval m in i * 2 seconden voor (int i = 1; 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

Zoals je kunt zien, hebben we de methode finish () verwijderd en hetzelfde draadbeëindigingsmechanisme geïmplementeerd met behulp van het ingebouwde onderbrekingssysteem. In deze implementatie hebben we één voordeel: de methode sleep () zal onmiddellijk terugkeren (een uitzondering maken) nadat de thread is onderbroken.

Houd er rekening mee dat de methoden sleep () en join () zijn verpakt in try-catch. Dit is een voorwaarde om deze methoden te laten werken. De aanroepende code moet de InterruptedException opvangen die ze gooien wanneer ze worden onderbroken tijdens het wachten.

We hebben het begin en einde van threads bedacht, daarna zal ik het hebben over de methoden die worden gebruikt bij het werken met threads.

Thread.sleep () methode

Thread.sleep () is een statische methode van de klasse Thread die de uitvoering van de thread waarin deze is aangeroepen, pauzeert. Tijdens de uitvoering van de sleep ()-methode stopt het systeem met het toewijzen van processortijd aan de thread en deze te verdelen over andere threads. De methode sleep () kan worden uitgevoerd gedurende een bepaalde tijd (milliseconden of nanoseconden) of totdat deze wordt gestopt door een interrupt (in welk geval een InterruptedException wordt gegenereerd).

Draad.slaap (1500); // Wacht anderhalve seconde Thread.sleep (2000, 100); // Wacht 2 seconden en 100 nanoseconden

Hoewel de sleep ()-methode nanoseconden als time-out kan nemen, moet u er niet lichtzinnig over doen. In veel systemen wordt de latentie nog steeds afgerond op milliseconden of zelfs tientallen milliseconden.

Opbrengst () methode

De statische methode Thread.yield () zorgt ervoor dat de processor overschakelt naar het verwerken van andere threads in het systeem. De methode kan bijvoorbeeld handig zijn wanneer een thread wacht op een gebeurtenis en het nodig is om zo vaak mogelijk te controleren of deze zich voordoet. In dit geval kunt u de gebeurteniscontrole en de methode Thread.yield () in een lus plaatsen:

// Wachten op bericht dat arriveert terwijl (! MsgQueue.hasMessages ()) // Terwijl er geen berichten in de wachtrij staan ​​(Thread.yield (); // Beheer overdragen aan andere threads)

Join () methode

Java biedt een mechanisme voor de ene thread om te wachten tot de andere is voltooid. Hiervoor wordt de methode join () gebruikt. Om bijvoorbeeld de hoofdthread te laten wachten tot de myThready in de zijthread is voltooid, moet u de instructie myThready.join () uitvoeren op de hoofdthread. Zodra de myThready-thread is voltooid, keert de methode join () terug en kan de hoofdthread doorgaan met de uitvoering.

De methode join () heeft een overbelasting die een time-out als parameter heeft. In dit geval retourneert join () wanneer de verwachte thread is voltooid of een time-out optreedt. Net als de methode Thread.sleep () kan de join-methode milliseconden en nanoseconden wachten - de argumenten zijn hetzelfde.

Door de thread-time-out in te stellen, kunt u bijvoorbeeld een geanimeerde afbeelding bijwerken terwijl de hoofd- (of een andere) thread wacht op de voltooiing van een zij-thread die resource-intensieve bewerkingen uitvoert:

Denkerbrein = nieuwe Denker (); // Thinker is een afstammeling van de Thread-klasse. brein.start (); // Begin met "denken". do (mThinkIndicator.refresh (); // mThinkIndicator is een geanimeerde afbeelding.try (brain.join (250); // Wacht een kwart seconde tot de gedachte stopt.) catch (InterruptedException e) ()) terwijl (hersenen.isAlive ()); // Terwijl de hersenen denken ... // hersenen is klaar met denken (er is een staande ovatie).

In dit voorbeeld denkt de hersenstroom ergens aan en wordt aangenomen dat het lang duurt. De hoofdthread wacht er een kwart seconde op en, als deze tijd voor reflectie niet genoeg is, werkt het de "reflectie-indicator" bij (een geanimeerde afbeelding). Als gevolg hiervan observeert de gebruiker tijdens het denken een indicator van het denkproces op het scherm, die hem laat weten dat het elektronische brein ergens mee bezig is.

Streamprioriteiten

Elke thread in het systeem heeft zijn eigen prioriteit. Prioriteit is een getal in een thread-object, waarvan een hogere waarde een hogere prioriteit betekent. Het systeem voert eerst de threads met een hogere prioriteit uit en de threads met een lagere prioriteit krijgen alleen CPU-tijd wanneer hun meer bevoorrechte broers en zussen inactief zijn.

U kunt met threadprioriteiten werken met behulp van twee functies:

void setPriority (int prioriteit)- stelt de prioriteit van de thread in.
Mogelijke prioriteitswaarden zijn MIN_PRIORITY, NORM_PRIORITY en MAX_PRIORITY.

int getPriority ()- krijgt de prioriteit van de draad.

Enkele nuttige methoden van de draadklasse

Dat is het eigenlijk wel. Ten slotte zijn hier enkele nuttige draadtechnieken.

boolean isAlive ()- geeft true terug als myThready () wordt uitgevoerd en false als de thread nog niet is gestart of is beëindigd.

setName (String threadName)- Specificeert de naam van de stream.
Tekenreeks getName ()- Haalt de naam van de stream op.
De naam van een thread is een bijbehorende string, wat in sommige gevallen helpt om te begrijpen welke thread een actie uitvoert. Dit is soms handig.

statische draad Thread.currentThread ()- een statische methode die het thread-object retourneert waarin het werd aangeroepen.

lang getId ()- geeft de stream-ID terug. ID is een uniek nummer dat aan de stream is toegewezen.

Gevolgtrekking

Merk op dat het artikel niet alle nuances van multithreaded programmeren behandelt. En de code die in de voorbeelden wordt gegeven, mist enkele nuances voor volledige correctheid. In de voorbeelden wordt met name geen synchronisatie gebruikt. Het synchroniseren van threads is een onderwerp zonder te leren dat u niet in staat zult zijn om correcte multithreaded applicaties te programmeren. U leest er bijvoorbeeld over in het boek "Java Concurrency in Practice" of