JavaRush /Blog Java /Random-PL /Zarządzanie przepływem. Słowo kluczowe volatile i metoda ...

Zarządzanie przepływem. Słowo kluczowe volatile i metoda rentowności().

Opublikowano w grupie Random-PL
Cześć! Kontynuujemy naukę wielowątkowości, a dziś poznamy nowe słowo kluczowe - volatile i metodę wydajności(). Ustalmy co to jest :)

Słowo kluczowe niestabilne

Tworząc aplikacje wielowątkowe, możemy napotkać dwa poważne problemy. Po pierwsze, podczas działania aplikacji wielowątkowej różne wątki mogą buforować wartości zmiennych (szerzej o tym porozmawiamy w wykładzie „Using volatile” ). Możliwe, że jeden wątek zmienił wartość zmiennej, ale drugi nie zauważył tej zmiany, ponieważ pracował z własną kopią zmiennej w pamięci podręcznej. Oczywiście konsekwencje mogą być poważne. Wyobraź sobie, że to nie jest tylko jakaś „zmienna”, ale na przykład saldo Twojej karty bankowej, które nagle zaczęło losowo przeskakiwać tam i z powrotem :) Niezbyt przyjemne, prawda? Po drugie, w Javie operacje odczytu i zapisu na polach wszystkich typów z wyjątkiem longi doublesą niepodzielne. Co to jest atomowość? No cóż, jeśli np. w jednym wątku zmienisz wartość zmiennej int, a w innym wątku odczytasz wartość tej zmiennej, to otrzymasz albo jej starą wartość, albo nową - tę, która wyszła po zmianie w wątek 1. Nie pojawią się tam żadne „opcje pośrednie”. Może. Jednak to nie działa z longi . doubleDlaczego? Ponieważ jest wieloplatformowy. Czy pamiętasz, jak powiedzieliśmy na pierwszych poziomach, że zasadę Java „napisano raz, działa wszędzie”? To jest wieloplatformowe. Oznacza to, że aplikacja Java działa na zupełnie innych platformach. Na przykład w systemach operacyjnych Windows, różnych wersjach Linuksa lub MacOS i wszędzie ta aplikacja będzie działać stabilnie. longoraz double- najbardziej „ciężkie” prymitywy w Javie: ważą 64 bity. A niektóre platformy 32-bitowe po prostu nie implementują atomowości odczytu i zapisu zmiennych 64-bitowych. Takie zmienne są odczytywane i zapisywane w dwóch operacjach. Najpierw do zmiennej zapisywane są pierwsze 32 bity, a następnie kolejne 32. W związku z tym w takich przypadkach może pojawić się problem. Jeden wątek zapisuje do zmiennej jakąś 64-bitową wartośćХi robi to „w dwóch krokach”. Jednocześnie drugi wątek próbuje odczytać wartość tej zmiennej i robi to w samym środku, gdy pierwsze 32 bity zostały już zapisane, ale drugie jeszcze nie zostały zapisane. W rezultacie odczytuje wartość pośrednią, niepoprawną i pojawia się błąd. Przykładowo jeśli na takiej platformie spróbujemy zapisać liczbę do zmiennej - 9223372036854775809 - zajmie ona 64 bity. W formie binarnej będzie to wyglądać tak: 1000000000000000000000000000000000000000000000000000000000001 Pierwszy wątek rozpocznie zapisywanie tej liczby do zmiennej i najpierw zapisze pierwsze 32 bity: 10000000000000000000000000000000000000000000 000000 00000 i potem drugi 32: 0000000000000000000000000000001 A drugi wątek może wcisnąć się w tę szczelinę i odczytaj wartość pośrednią zmiennej - 100000000000000000000000000000000, pierwsze 32 bity, które zostały już zapisane. W systemie dziesiętnym liczba ta jest równa 2147483648. Oznacza to, że chcieliśmy po prostu zapisać liczbę 9223372036854775809 do zmiennej, ale ze względu na to, że ta operacja na niektórych platformach nie jest atomowa, otrzymaliśmy „lewą” liczbę 2147483648 , którego nie potrzebujemy, znikąd i nie wiadomo, jak wpłynie to na działanie programu. Drugi wątek po prostu odczytał wartość zmiennej przed jej ostatecznym zapisaniem, to znaczy zobaczył pierwsze 32 bity, ale nie drugie 32 bity. Problemy te oczywiście nie pojawiły się wczoraj i w Javie rozwiązuje się je za pomocą tylko jednego słowa kluczowego - volatile . Jeśli zadeklarujemy w naszym programie jakąś zmienną słowem volatile...
public class Main {

   public volatile long x = 2222222222222222222L;

   public static void main(String[] args) {

   }
}
…to znaczy, że:
  1. Zawsze będzie odczytywany i zapisywany atomowo. Nawet jeśli jest to wersja 64-bitowa doublelub long.
  2. Maszyna Java nie będzie go buforować. Wykluczona jest zatem sytuacja, gdy 10 wątków działa na swoich lokalnych kopiach.
Tak jednym słowem rozwiązuje się dwa bardzo poważne problemy :)

metoda wydajności().

Przyjrzeliśmy się już wielu metodom tej klasy Thread, ale jest jedna ważna, która będzie dla Ciebie nowa. To jest metoda rentowności() . Przetłumaczone z angielskiego jako „poddawać się”. I właśnie na tym polega ta metoda! Zarządzanie przepływem.  Słowo kluczowe volatile i metoda rentowności() - 2Kiedy wywołujemy metodę Yield w wątku, tak naprawdę mówi to innym wątkom: „OK, chłopaki, nie spieszy mi się szczególnie, więc jeśli dla kogokolwiek z was ważne jest, aby uzyskać czas procesora, niech to weźmie, ja nie pilne." Oto prosty przykład działania:
public class ThreadExample extends Thread {

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

   public void run() {

       System.out.println(Thread.currentThread().getName() + „ustąpić miejsca innym”);
       Thread.yield();
       System.out.println(Thread.currentThread().getName() + " has finished executing.");
   }

   public static void main(String[] args) {
       new ThreadExample();
       new ThreadExample();
       new ThreadExample();
   }
}
Kolejno tworzymy i uruchamiamy trzy wątki Thread-0- Thread-1i Thread-2. Thread-0zaczyna się jako pierwszy i natychmiast ustępuje miejsca innym. Po tym zaczyna się Thread-1i również ustępuje. Potem się zaczyna Thread-2, co również jest gorsze. Nie mamy już żadnych wątków, a kiedy Thread-2ostatni ustąpił miejsca, planista wątków wygląda: „No to nie ma już nowych wątków, kogo mamy w kolejce? Kto jako ostatni ustąpił wcześniej swojego miejsca Thread-2? Myślę, że tak było Thread-1? OK, więc niech tak się stanie.” Thread-1wykonuje swoje zadanie do końca, po czym program planujący wątki kontynuuje koordynację: „OK, wątek-1 został zakończony. Czy mamy jeszcze kogoś w kolejce?” W kolejce znajduje się wątek-0: ustąpił miejsca bezpośrednio przed wątkiem-1. Teraz sprawa do niego dotarła i jest on prowadzony do końca. Po czym planista kończy koordynację wątków: „OK, wątek-2, ustąpiłeś miejsca innym wątkom, wszystkie już zadziałały. Byłeś ostatnim, który ustąpił, więc teraz twoja kolej. Następnie Thread-2 działa do końca. Dane wyjściowe konsoli będą wyglądać następująco: Wątek-0 ustępuje innym Wątek-1 ustępuje innym Wątek-2 ustępuje innym Wątek-1 zakończył wykonywanie. Wątek-0 zakończył wykonywanie. Wątek-2 zakończył wykonywanie. Harmonogram wątków może oczywiście uruchamiać wątki w innej kolejności (na przykład 2-1-0 zamiast 0-1-2), ale zasada jest taka sama.

Dzieje się przed regułami

Ostatnią rzeczą, którą dzisiaj poruszymy, jest zasada „ zdarza się wcześniej ”. Jak już wiesz, w Javie większość pracy związanej z przydzielaniem czasu i zasobów wątkom w celu wykonania ich zadań jest wykonywana przez planistę wątków. Ponadto nie raz widziałeś, jak wątki są wykonywane w dowolnej kolejności i najczęściej nie da się tego przewidzieć. I ogólnie, po programowaniu „sekwencyjnym”, które zrobiliśmy wcześniej, wielowątkowość wygląda na przypadkową rzecz. Jak już widziałeś, postęp programu wielowątkowego można kontrolować za pomocą całego zestawu metod. Ale oprócz tego w wielowątkowości Java istnieje jeszcze jedna „wyspa stabilności” - 4 reguły zwane „ zdarza się wcześniej ”. Dosłownie z języka angielskiego tłumaczy się to jako „zdarza się wcześniej” lub „zdarza się wcześniej”. Znaczenie tych zasad jest dość proste do zrozumienia. Wyobraź sobie, że mamy dwa wątki - Ai B. Każdy z tych wątków może wykonywać operacje 1i 2. A kiedy w każdej z reguł mówimy „ A dzieje się przed B ”, oznacza to, że wszystkie zmiany dokonane przez wątek Aprzed operacją 1oraz zmiany, które ta operacja pociągała za sobą, są widoczne dla wątku Bw momencie wykonania operacji 2i po wykonaniu operacji. Każda z tych reguł zapewnia, że ​​podczas pisania programu wielowątkowego w 100% przypadków niektóre zdarzenia wystąpią przed innymi i że wątek Bw czasie operacji 2będzie zawsze świadomy zmian, jakie wątek Аwprowadził podczas operacji 1. Przyjrzyjmy się im.

Zasada nr 1.

Zwolnienie muteksu następuje zanim inny wątek uzyska ten sam monitor. Cóż, tutaj wszystko wydaje się jasne. Jeśli muteks obiektu lub klasy zostanie przejęty przez jeden wątek, na przykład wątek А, inny wątek (wątek B) nie może go uzyskać w tym samym czasie. Musisz poczekać, aż muteks zostanie zwolniony.

Zasada 2.

Metoda Thread.start() dzieje się przed Thread.run() . Nic skomplikowanego. Już wiesz: aby kod znajdujący się w metodzie zaczął się wykonywać run(), musisz wywołać metodę w wątku start(). To jego, a nie sama metoda run()! Ta reguła gwarantuje, że Thread.start()wartości wszystkich zmiennych ustawionych przed wykonaniem będą widoczne wewnątrz metody, która rozpoczęła wykonywanie run().

Zasada 3.

Zakończenie metody run() następuje przed zakończeniem metody join(). Wróćmy do naszych dwóch strumieni - Аi B. Metodę wywołujemy join()w taki sposób, że wątek Bmusi poczekać na zakończenie Azanim zacznie swoją pracę. Oznacza to, że metoda run()obiektu A na pewno będzie działać do samego końca. Wszystkie zmiany danych zachodzące w metodzie run()wątku Abędą całkowicie widoczne w wątku, Bgdy wątek będzie czekał na zakończenie Ai sam zacznie działać.

Zasada 4.

Zapisywanie do zmiennej niestabilnej następuje przed odczytaniem tej samej zmiennej. Używając słowa kluczowego volatile, tak naprawdę zawsze otrzymamy aktualną wartość. Nawet w przypadku longi double, z którymi problemy były omawiane wcześniej. Jak już wiesz, zmiany wprowadzone w niektórych wątkach nie zawsze są widoczne dla innych wątków. Ale oczywiście bardzo często zdarzają się sytuacje, gdy takie zachowanie programu nam nie odpowiada. Załóżmy, że przypisaliśmy wartość zmiennej w wątku A:
int z;.

z= 555;
Jeśli nasz wątek Bmiałby wydrukować wartość zmiennej zna konsoli, mógłby z łatwością wydrukować 0, ponieważ nie wie o przypisanej do niego wartości. Zatem Zasada 4 gwarantuje nam: jeśli zadeklarujesz zmienną zjako zmienną, zmiany jej wartości w jednym wątku będą zawsze widoczne w innym wątku. Jeśli do poprzedniego kodu dodamy słowo volatile...
volatile int z;.

z= 555;
...wykluczona jest sytuacja, w której strumień Bwyprowadzi na konsolę wartość 0. Zapisywanie do zmiennych niestabilnych następuje przed ich odczytaniem.
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION