pričekajte i obavijestite () Metode u Javi

1. Uvod

U ovom ćemo članku razmotriti jedan od najvažnijih mehanizama u Javi - sinkronizaciju niti.

Prvo ćemo razmotriti neke bitne pojmove i metodologije povezane s istodobnošću.

I mi ćemo razviti jednostavnu aplikaciju - gdje ćemo rješavati probleme istovremenosti, s ciljem boljeg razumijevanja čekati() i obavijestiti().

2. Sinkronizacija niti u Javi

U višenitnom okruženju, više niti može pokušati modificirati isti resurs. Ako se nitima ne upravlja pravilno, to će, naravno, dovesti do problema s dosljednošću.

2.1. Zaštićeni blokovi u Javi

Jedan alat koji možemo koristiti za koordinaciju radnji više niti u Javi - zaštićeni su blokovi. Takvi blokovi provjeravaju određeno stanje prije nastavka izvršenja.

Imajući to na umu, iskoristit ćemo:

  • Object.wait () - za suspendiranje niti
  • Object.notify () - probuditi nit

To se može bolje razumjeti iz sljedećeg dijagrama koji prikazuje životni ciklus a Nit:

Imajte na umu da postoji mnogo načina upravljanja ovim životnim ciklusom; međutim, u ovom ćemo se članku usredotočiti samo na čekati() i obavijestiti().

3. The čekati() Metoda

Jednostavno rečeno, kad nazovemo pričekaj () - to prisiljava trenutnu nit da pričeka dok se ne pozove neka druga nit obavijestiti() ili notifyAll () na istom objektu.

Za to trenutna nit mora biti vlasnik monitora objekta. Prema Javadocsu, to se može dogoditi kada:

  • pogubili smo sinkronizirano metoda instance za zadani objekt
  • pogubili smo tijelo a sinkronizirano blok na zadanom objektu
  • izvršavanjem sinkronizirani statički metode za objekte tipa Razred

Imajte na umu da samo jedna aktivna nit može istovremeno posjedovati monitor objekta.

Ovaj čekati() metoda dolazi s tri preopterećena potpisa. Pogledajmo ovo.

3.1. čekati()

The čekati() metoda uzrokuje da trenutna nit čeka neograničeno dok se druga niti ne pozove obavijestiti() za ovaj objekt odn notifyAll ().

3.2. pričekati (dugo istek)

Pomoću ove metode možemo odrediti vremensko ograničenje nakon kojeg će se nit automatski probuditi. Nit se može probuditi prije nego što dostigne vremensko ograničenje pomoću obavijestiti() ili notifyAll ().

Imajte na umu da pozivanje pričekaj (0) je isto što i pozivanje čekati().

3.3. pričekati (dugo čekanje, int nanos)

Ovo je još jedan potpis koji pruža istu funkciju, s jedinom razlikom u tome što možemo pružiti veću preciznost.

Ukupno razdoblje vremenskog ograničenja (u nanosekundama) izračunava se kao 1_000_000 * vremensko ograničenje + nanos.

4. obavijestiti () i notifyAll ()

The obavijestiti() metoda koristi se za buđenje niti koje čekaju pristup monitoru ovog objekta.

Postoje dva načina obavještavanja o nitima na čekanju.

4.1. obavijestiti()

Za sve niti koje čekaju na monitoru ovog objekta (korištenjem bilo kojeg od čekati() metoda), metoda obavijestiti() obavještava bilo koga od njih da se proizvoljno probudi. Izbor točno koje niti za buđenje nije deterministički i ovisi o implementaciji.

Od obavijestiti() probudi jednu slučajnu nit može se koristiti za provedbu međusobno isključivog zaključavanja gdje niti rade slične zadatke, ali u većini slučajeva bilo bi isplativije provesti notifyAll ().

4.2. notifyAll ()

Ova metoda jednostavno budi sve niti koje čekaju na monitoru ovog objekta.

Probuđene niti završit će se na uobičajeni način - poput bilo koje druge niti.

Ali prije nego što dopustimo da se njihovo izvršenje nastavi, uvijek definirajte brzu provjeru stanja potrebnog za nastavak niti - jer mogu biti situacije u kojima se nit probudila bez primanja obavijesti (ovaj je scenarij razmatran kasnije u primjeru).

5. Problem sinkronizacije pošiljatelja i primatelja

Sad kad razumijemo osnove, idemo kroz jedno jednostavno PošiljateljPrijamnik aplikacija - koja će koristiti čekati() i obavijestiti() metode za postavljanje sinkronizacije između njih:

  • The Pošiljatelj trebao bi poslati paket podataka na Prijamnik
  • The Prijamnik ne može obraditi podatkovni paket sve dok Pošiljatelj je završio s slanjem
  • Slično tome, Pošiljatelj ne smije pokušati poslati novi paket osim ako Prijamnik je već obradio prethodni paket

Prvo stvorimo Podaci klasa koja se sastoji od podataka paket koji će biti poslan iz Pošiljatelj do Prijamnik. Koristit ćemo čekati() i notifyAll () da biste postavili sinkronizaciju između njih:

podaci javne klase {privatni niz paketa; // Tačno ako primatelj treba pričekati // Netačno ako pošiljatelj treba pričekati private boolean transfer = true; javno sinkronizirano void slanje (niz paketa) {while (! transfer) {try {wait (); } catch (InterruptedException e) {Thread.currentThread (). interrupt (); Log.error ("Navoj prekinut", e); }} prijenos = netačno; this.packet = paket; notifyAll (); } javni sinkronizirani niz primiti () {dok (prijenos) {pokušati {pričekati (); } catch (InterruptedException e) {Thread.currentThread (). interrupt (); Log.error ("Navoj prekinut", e); }} prijenos = istina; notifyAll (); povratni paket; }}

Razdvojimo što se ovdje događa:

  • The paket varijabla označava podatke koji se prenose mrežom
  • Imamo boolean varijabilna prijenos - koje je Pošiljatelj i Prijamnik za sinkronizaciju će koristiti:
    • Ako je ova varijabla pravi, onda Prijamnik treba pričekati Pošiljatelj za slanje poruke
    • Ako jest lažno, onda Pošiljatelj treba pričekati Prijamnik za primanje poruke
  • The Pošiljatelj koristi poslati() metoda za slanje podataka na Prijamnik:
    • Ako prijenos je lažno, čekat ćemo pozivom čekati() na ovoj niti
    • Ali kad je pravi, prebacujemo status, postavljamo svoju poruku i pozivamo notifyAll () da probude druge niti kako bi naveli da se dogodio značajan događaj i mogu provjeriti mogu li nastaviti s izvršavanjem
  • Slično tome, Prijamnik koristit ću primiti () metoda:
    • Ako je prijenos je postavljeno na lažno po Pošiljatelj, tada će se samo nastaviti, inače ćemo nazvati čekati() na ovoj niti
    • Kad je uvjet ispunjen, prebacujemo status, obavještavamo sve niti koje čekaju da se probude i vraćamo podatkovni paket koji je bio Prijamnik

5.1. Zašto priložiti čekati() u dok Petlja?

Od obavijestiti() i notifyAll () nasumično probudi niti koje čekaju na monitoru ovog objekta, nije uvijek važno da je uvjet zadovoljen. Ponekad se može dogoditi da se nit probudi, ali uvjet zapravo još nije zadovoljen.

Također možemo definirati provjeru da nas spasi od lažnih buđenja - gdje se nit može probuditi od čekanja, a da nikada nije primila obavijest.

5.2. Zašto trebamo sinkronizirati skraj() i primiti () Metode?

Stavili smo ove metode unutra sinkronizirano metode za osiguravanje unutarnjih brava. Ako nit poziva čekati() metoda ne posjeduje inherentnu bravu, pojavit će se pogreška.

Sad ćemo stvoriti Pošiljatelj i Prijamnik i provesti Izvodljivo sučelje na obje, tako da njihove instance mogu biti izvedene pomoću niti.

Prvo da vidimo kako Pošiljatelj će raditi:

javna klasa pošiljatelj implementira Runnable {private Data data; // standardni konstruktori public void run () {String paketi [] = {"Prvi paket", "Drugi paket", "Treći paket", "Četvrti paket", "Kraj"}; za (Niz paketa: paketi) {data.send (paket); // Thread.sleep () za oponašanje teške obrade na strani poslužitelja pokušajte {Thread.sleep (ThreadLocalRandom.current (). NextInt (1000, 5000)); } catch (InterruptedException e) {Thread.currentThread (). interrupt (); Log.error ("Navoj prekinut", e); }}}}

Za ovo Pošiljatelj:

  • Stvaramo neke slučajne pakete podataka koji će se slati mrežom u paketi [] niz
  • Za svaki paket samo zovemo poslati()
  • Onda zovemo Thread.sleep () sa slučajnim intervalom za oponašanje teške obrade na strani poslužitelja

Napokon, provedimo naše Prijamnik:

prijemnik javne klase implementira Runnable {private Data load; // standardni konstruktori public void run () {for (String receivedMessage = load.receive ();! "End" .equals (receivedMessage); receivedMessage = load.receive ()) {System.out.println (полученMessage); // ... probajte {Thread.sleep (ThreadLocalRandom.current (). nextInt (1000, 5000)); } catch (InterruptedException e) {Thread.currentThread (). interrupt (); Log.error ("Navoj prekinut", e); }}}}

Evo, jednostavno zovemo load.receive () u petlji dok ne dobijemo posljednju "Kraj" podatkovni paket.

Pogledajmo sada ovu aplikaciju na djelu:

javna statička void glavna (String [] args) {Podaci podataka = novi podaci (); Pošiljatelj niti = nova nit (novi pošiljatelj (podaci)); Primatelj niti = nova nit (novi prijemnik (podaci)); sender.start (); prijemnik.start (); }

Primit ćemo sljedeći izlaz:

Prvi paket Drugi paket Treći paket Četvrti paket 

I evo nas - primili smo sve podatkovne pakete u pravom, sekvencijalnom redoslijedu i uspješno uspostavili ispravnu komunikaciju između našeg pošiljatelja i primatelja.

6. Zaključak

U ovom smo članku razgovarali o nekim osnovnim konceptima sinkronizacije u Javi; točnije, usredotočili smo se na to kako možemo koristiti čekati() i obavijestiti() za rješavanje zanimljivih problema sinkronizacije. I na kraju, prošli smo kroz uzorak koda gdje smo primijenili ove koncepte u praksi.

Prije nego što ovdje završimo, vrijedi spomenuti da su svi ti API-ji niske razine, kao što su čekati(), obavijestiti() i notifyAll () - su tradicionalne metode koje dobro rade, ali mehanizmi više razine često su jednostavniji i bolji - kao što je izvorni Java Zaključaj i Stanje sučelja (dostupno u java.util.concurrent.locks paket).

Za više informacija o java.util.concurrent paket, posjetite naš pregled članka java.util.concurrent i Zaključaj i Stanje pokriveni su u vodiču za java.util.concurrent.Locks, ovdje.

Kao i uvijek, cjeloviti isječci koda korišteni u ovom članku dostupni su na GitHubu.