CodeGym /Java blog /Véletlen /Szálak kezelése. A volatile kulcsszó és a hozam() metódus...
John Squirrels
Szint
San Francisco

Szálak kezelése. A volatile kulcsszó és a hozam() metódus

Megjelent a csoportban
Szia! Folytatjuk a többszálúság vizsgálatát. Ma megismerjük a volatilekulcsszót és a yield()módszert. Merüljünk bele :)

Az illékony kulcsszó

Többszálú alkalmazások készítésekor két komoly problémába ütközhetünk. Először is, amikor egy többszálú alkalmazás fut, a különböző szálak gyorsítótárazhatják a változók értékeit (erről már beszéltünk a 'A volatile használata' című leckében ). Előfordulhat olyan helyzet, hogy az egyik szál megváltoztatja egy változó értékét, de a második szál nem látja a változást, mert a változó gyorsítótárazott másolatával dolgozik. Természetesen a következmények súlyosak lehetnek. Tegyük fel, hogy ez nem egy régi változó, hanem a bankszámla egyenlege, ami hirtelen véletlenszerűen kezd fel-le ugrálni :) Ez nem hangzik valami szórakoztatónak, igaz? Másodszor, a Java-ban az összes primitív típus olvasására és írására szolgáló műveletek,longdouble, atomosak. Nos, ha például az egyik szálon megváltoztatod egy intváltozó értékét, egy másik szálon pedig kiolvasod a változó értékét, akkor vagy a régi értékét kapod, vagy az újat, vagyis azt az értéket, ami a változásból adódott. az 1. szálban. Nincsenek 'köztes értékek'. Ez azonban nem működik longs és doubles esetén. Miért? A platformok közötti támogatás miatt. Emlékszel a kezdeti szinteken, hogy azt mondtuk, hogy a Java vezérelve az „egyszer írd, futtasd bárhol”? Ez többplatformos támogatást jelent. Más szóval, a Java alkalmazások mindenféle platformon futnak. Például Windows operációs rendszereken, Linux vagy MacOS különböző verzióiban. Probléma nélkül fog futni mindegyiken. 64 bites súlyú,longdoublea „legnehezebb” primitívek a Java nyelven. Bizonyos 32 bites platformok pedig egyszerűen nem valósítják meg a 64 bites változók atomi olvasását és írását. Az ilyen változók olvasása és írása két művelettel történik. Először az első 32 bitet írjuk a változóba, majd további 32 bitet írunk. Ennek eredményeként probléma merülhet fel. Az egyik szál valamilyen 64 bites értéket ír egy változóba X, és ezt két művelettel teszi meg. Ugyanakkor egy második szál megpróbálja beolvasni a változó értékét, és ezt a két művelet között teszi meg – amikor az első 32 bitet írták, de a második 32 bitet nem. Ennek eredményeként egy köztes, helytelen értéket olvas, és van egy hibánk. Például, ha egy ilyen platformon megpróbáljuk beírni a számot egy 9223372036854775809 változóhoz, akkor 64 bitet fog elfoglalni. Bináris formában ez így néz ki: 1000000000000000000000000000000000000000000000000000000000000001 Az első szál elkezdi írni a számot a változóhoz. Eleinte az első 32 bitet (10000000000000000000000000000) , majd a második 32 bitet (000000000000000000000000000000001) írja. És a második szál ébredhet e műveletek között, elolvasta a változó közbenső értékét (100000000000000000000000000000), amelyek az első 32 bit, amelyet már megírtak. A decimális rendszerben ez a szám 2 147 483 648. Vagyis csak a 9223372036854775809 számot akartuk beírni egy változóba, de mivel ez a művelet bizonyos platformokon nem atomi, megvan a 2 147 483 648 gonosz szám, ami a semmiből jött és ismeretlen hatással lesz a program. A második szál egyszerűen beolvasta a változó értékét, mielőtt befejezte volna az írást, azaz a szál látta az első 32 bitet, de a második 32 bitet nem. Ezek a problémák persze nem tegnap jelentkeztek. A Java egyetlen kulcsszóval oldja meg őket: volatile. Ha használjuk avolatilekulcsszó, amikor valamilyen változót deklarálunk a programunkban…

public class Main {

   public volatile long x = 2222222222222222222L;

   public static void main(String[] args) {

   }
}
…ez azt jelenti:
  1. Mindig atomosan lesz olvasható és írva. Még akkor is, ha 64 bites doublevagy long.
  2. A Java gép nem fogja gyorsítótárazni. Így nem lesz olyan helyzet, amikor 10 szál a saját helyi másolataival dolgozik.
Így két nagyon komoly probléma megoldódik egyetlen szóval :)

A hozam() módszer

Az osztály számos módszerét már áttekintettük Thread, de van egy fontos módszer, amely újdonság lesz számodra. Ez a yield()módszer . És pontosan azt teszi, amit a neve is sugall! Szálak kezelése.  A volatile kulcsszó és a hozam() metódus - 2Amikor meghívjuk a yieldmetódust egy szálon, az valójában a többi szálhoz beszél: „Hé, srácok. Nem sietek különösebben sehova, úgyhogy ha valakinek fontos, hogy processzoridőhöz jusson, vegye ki – várhatok. Íme egy egyszerű példa ennek működésére:

public class ThreadExample extends Thread {

   public ThreadExample() {
       this.start();
   }

   public void run() {

       System.out.println(Thread.currentThread().getName() + " yields its place to others");
       Thread.yield();
       System.out.println(Thread.currentThread().getName() + " has finished executing.");
   }

   public static void main(String[] args) {
       new ThreadExample();
       new ThreadExample();
       new ThreadExample();
   }
}
Három szálat hozunk létre és indítunk el egymás után: Thread-0, Thread-1, és Thread-2. Thread-0először indul és azonnal enged a többieknek. Aztán Thread-1elindul és hozam is. Ezután Thread-2elindul, ami szintén hozamot hoz. Nincs több szálunk, és miután Thread-2utoljára átadta a helyét, a szálütemező azt mondja: „Hmm, nincs több új szál. Kik állnak a sorban? Ki adta át a helyét korábban Thread-2? Úgy tűnik, az volt Thread-1. Oké, ez azt jelenti, hogy hagyjuk futni. Thread-1befejezi a munkáját, majd a szálütemező folytatja a koordinációt: „Rendben, Thread-1kész. Van még valaki a sorban?' A 0-s szál a sorban van: pont azelőtt adta át a helyétThread-1. Most rákerül a sor, és befut a befejezésig. Ezután az ütemező befejezi a szálak koordinálását: „Rendben, Thread-2, engedett a többi szálnak, és most már minden kész. Te voltál az utolsó, aki engedett, tehát most rajtad a sor. Ezután Thread-2fut a befejezésig. A konzol kimenete így fog kinézni: A 0. szál átadja a helyét másoknak Az 1. szál átadja a helyét másoknak A 2. szál átadja a helyét másoknak Az 1. szál végrehajtása befejeződött. A 0. szál végrehajtása befejeződött. A 2. szál végrehajtása befejeződött. Természetesen a szálütemező elindíthatja a szálakat más sorrendben (például 2-1-0 a 0-1-2 helyett), de az elv ugyanaz marad.

Történik – a szabályok előtt

Az utolsó dolog, amit ma érintünk, az a „ korábban történik ” fogalma. Amint azt már tudja, a Java-ban a szálütemező végzi el az idő és az erőforrások szálakhoz való hozzárendelésével kapcsolatos munka nagy részét feladataik elvégzéséhez. Azt is többször láttad, hogy a szálak véletlenszerű sorrendben futnak, amit általában lehetetlen előre megjósolni. Általánosságban elmondható, hogy a „szekvenciális” programozás után, amit korábban csináltunk, a többszálú programozás véletlenszerűnek tűnik. Már azt hitte, hogy számos módszert használhat a többszálú programok áramlásának szabályozására. De a Java többszálas működésének van még egy pillére – a 4 „ előtte történik ” szabályok. Ezeknek a szabályoknak a megértése meglehetősen egyszerű. Képzeljük el, hogy két szálunk van – AésB. Ezen szálak mindegyike végrehajthat műveleteket 1és 2. Minden szabályban, amikor azt mondjuk, hogy „ A megtörténik-B előtt ”, akkor azt értjük, hogy a szál által Aa művelet előtt végrehajtott összes változtatás 1, és az ebből a műveletből származó változtatások láthatóak a szál számára a művelet végrehajtása Bsorán és azt követően. 2Mindegyik szabály garantálja, hogy többszálú program írásakor bizonyos események az esetek 100%-ában előbb történnek meg, mint a többiek, és hogy a műveleti 2szál Bmindig tudatában lesz a szál által Aa művelet során végrehajtott változtatásoknak 1. Tekintsük át őket.

1. szabály

A mutex feloldása azelőtt megtörténik, hogy ugyanazt a monitort egy másik szál megszerezné. Szerintem itt mindent értesz. Ha egy objektum vagy osztály mutexét egy szál szerzi meg, például a szál A, akkor egy másik szál (szál B) nem tudja egyszerre megszerezni. Meg kell várnia, amíg a mutex felszabadul.

2. szabály

A Thread.start()módszer korábban történik Thread.run() . Itt megint nincs semmi nehéz. Már tudja, hogy a metóduson belüli kód futtatásához run()meg kell hívnia a start()metódust a szálon. Pontosabban a start metódus, nem run()maga a metódus! Ez a szabály biztosítja, hogy a hívás előtt beállított összes változó értéke Thread.start()látható legyen a run()metóduson belül, amint elkezdődik.

3. szabály.

A run()metódus vége a metódusból való visszatérés előtt történikjoin() . Térjünk vissza a két szálunkhoz: Aés B. A metódust úgy hívjuk meg join(), hogy a szál Bgarantáltan megvárja a szál befejezését, Amielőtt elvégezné a munkáját. Ez azt jelenti, hogy az A objektum run()metódusa garantáltan a végéig fut. És minden adatmódosítás, amely a run()szál metódusában történik A, száz százalékig garantáltan látható lesz a szálban, Bmiután befejezte a szál befejezését, Aés megkezdheti saját munkáját.

4. szabály.

A volatileváltozóba való írás azelőtt történik, hogy ugyanabból a változóból olvasna. Amikor a kulcsszót használjuk volatile, valójában mindig az aktuális értéket kapjuk. Akár a longvagy -val doubleis (korábban beszéltünk az itt előforduló problémákról). Amint azt már megérti, bizonyos szálakon végrehajtott módosítások nem mindig láthatók más szálak számára. De természetesen nagyon gyakran vannak olyan helyzetek, amikor az ilyen viselkedés nem illik hozzánk. Tegyük fel, hogy értéket rendelünk egy változóhoz a szálon A:

int z;

….

z = 555;
Ha a Bszálunk a zváltozó értékét jelenítené meg a konzolon, könnyen 0-t jeleníthet meg, mert nem tud a hozzárendelt értékről. zDe a 4. szabály garantálja, hogy ha a változót ként deklaráljuk volatile, akkor az értékének változásai az egyik szálon mindig láthatóak lesznek a másik szálon. Ha hozzáadjuk volatileaz előző kódhoz tartozó szót...

volatile int z;

….

z = 555;
...akkor megakadályozzuk azt a helyzetet, hogy a szál B0-t jelenítsen meg. volatileA változók írása az olvasás előtt történik.
Hozzászólások
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION