CodeGym /Blog Java /Random-PL /Zarządzanie wątkami. Słowo kluczowe volatile i metoda yie...
Autor
Volodymyr Portianko
Java Engineer at Playtika

Zarządzanie wątkami. Słowo kluczowe volatile i metoda yield().

Opublikowano w grupie Random-PL
Cześć! Kontynuujemy nasze badanie wielowątkowości. Dziś poznamy słowo volatilekluczowe i yield()metodę. Zanurzmy się :)

Niestabilne słowo kluczowe

Podczas tworzenia aplikacji wielowątkowych możemy napotkać dwa poważne problemy. Po pierwsze, gdy uruchomiona jest aplikacja wielowątkowa, różne wątki mogą buforować wartości zmiennych (mówiliśmy już o tym w lekcji zatytułowanej „Używanie volatile” ). Może dojść do sytuacji, w której jeden wątek zmienia wartość zmiennej, ale drugi wątek nie widzi tej zmiany, ponieważ pracuje z buforowaną kopią zmiennej. Oczywiście konsekwencje mogą być poważne. Załóżmy, że nie jest to jakaś stara zmienna, ale saldo konta bankowego, które nagle zaczyna losowo skakać w górę iw dół :) To nie brzmi jak zabawa, prawda? Po drugie, w Javie operacje odczytu i zapisu wszystkich typów pierwotnych,longdouble, są atomowe. Otóż ​​jeśli np. zmienisz wartość zmiennej intw jednym wątku, a w innym wątku odczytasz wartość zmiennej, to albo otrzymasz jej starą wartość, albo nową, czyli taką, która wynikała ze zmiany w wątku 1. Nie ma „wartości pośrednich”. Jednak to nie działa z longs i doubles. Dlaczego? Ze względu na obsługę wielu platform. Pamiętasz, jak na początkowych poziomach powiedzieliśmy, że główną zasadą Javy jest „napisz raz, uruchom gdziekolwiek”? Oznacza to obsługę wielu platform. Innymi słowy, aplikacja Java działa na różnego rodzaju platformach. Na przykład w systemach operacyjnych Windows, różnych wersjach systemu Linux lub MacOS. Będzie działać bez żadnych problemów na wszystkich. Ważenie w 64 bitach,longdoublesą „najcięższymi” prymitywami w Javie. A niektóre platformy 32-bitowe po prostu nie implementują atomowego 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 bity. W rezultacie może pojawić się problem. Jeden wątek zapisuje pewną 64-bitową wartość do Xzmiennej i robi to w dwóch operacjach. W tym samym czasie drugi wątek próbuje odczytać wartość zmiennej i robi to pomiędzy tymi dwiema operacjami — gdy pierwsze 32 bity zostały zapisane, ale drugie 32 bity nie. W rezultacie odczytuje pośrednią, niepoprawną wartość i mamy błąd. Na przykład, jeśli na takiej platformie spróbujemy wpisać numer na 9223372036854775809 do zmiennej, zajmie 64 bity. W postaci binarnej wygląda to tak: 100000000000000000000000000000000000000000000000000000000000000001 Pierwszy wątek zaczyna zapisywać liczbę do zmiennej. Najpierw zapisuje pierwsze 32 bity (10000000000000000000000000000000) , a następnie drugie 32 bity (0000000000000000000000000000001) A drugi wątek może zaklinować się między tymi operacjami, odczytując wartość pośrednią zmiennej (10000000000000000000000000000000), czyli pierwsze 32 bity, które zostały już zapisane. W systemie dziesiętnym liczba ta wynosi 2 147 483 648. Innymi słowy, chcieliśmy po prostu zapisać liczbę 9223372036854775809 do zmiennej, ale ze względu na fakt, że ta operacja nie jest atomowa na niektórych platformach, mamy złą liczbę 2 147 483 648, która pojawiła się znikąd i będzie miała nieznany efekt program. Drugi wątek po prostu odczytał wartość zmiennej, zanim skończyło się zapisywanie, tzn. wątek widział pierwsze 32 bity, ale nie widział drugich 32 bitów. Oczywiście te problemy nie pojawiły się wczoraj. Java rozwiązuje je za pomocą jednego słowa kluczowego: volatile. Jeśli korzystamy zvolatilesłowo kluczowe podczas deklarowania jakiejś zmiennej w naszym programie…

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ć. Nie będziesz więc miał sytuacji, w której 10 wątków pracuje z własnymi lokalnymi kopiami.
W ten sposób dwa bardzo poważne problemy można rozwiązać jednym słowem :)

Metoda yield().

Omówiliśmy już wiele Threadmetod tej klasy, ale jest jedna ważna metoda, która będzie dla Ciebie nowa. To jest yield()metoda . I robi dokładnie to, co sugeruje jego nazwa! Zarządzanie wątkami.  Słowo kluczowe volatile i metoda yield() - 2Kiedy wywołujemy yieldmetodę w wątku, w rzeczywistości komunikuje się ona z innymi wątkami: „Hej, chłopaki. Nigdzie mi się szczególnie nie spieszy, więc jeśli komuś z was zależy na czasie procesora, niech to weźmie — mogę zaczekać”. Oto prosty przykład, jak to działa:

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();
   }
}
Kolejno tworzymy i uruchamiamy trzy wątki: Thread-0, Thread-1, i Thread-2. Thread-0zaczyna pierwszy i natychmiast ustępuje innym. Następnie Thread-1zaczyna się i również plonuje. Następnie Thread-2zaczyna się, co również daje. Nie mamy więcej wątków, a po Thread-2ustąpieniu ostatniego miejsca program planujący wątki mówi: „Hmm, nie ma już żadnych nowych wątków. Kogo mamy w kolejce? Kto wcześniej ustąpił miejsca Thread-2? Wydaje się, że tak było Thread-1. Dobra, to znaczy, że pozwolimy temu działać”. Thread-1kończy swoją pracę, a następnie program planujący wątki kontynuuje koordynację: „Dobrze, Thread-1gotowe. Czy mamy jeszcze kogoś w kolejce? Thread-0 jest w kolejce: wcześniej ustąpił miejscaThread-1. Teraz ma swoją kolej i biegnie do końca. Następnie program planujący kończy koordynację wątków: „Dobrze, Thread-2poddałeś się innym wątkom i wszystkie są już gotowe. Byłeś ostatnim, który się poddał, więc teraz twoja kolej”. Następnie Thread-2biegnie do końca. Dane wyjściowe konsoli będą wyglądać następująco: Wątek-0 ustępuje miejsca innym Wątek-1 ustępuje miejsca innym Wątek-2 ustępuje miejsca innym, których działanie Wątek-1 zakończył. Wątek-0 zakończył wykonywanie. Wątek-2 zakończył wykonywanie. Oczywiście harmonogram wątków może uruchamiać wątki w innej kolejności (na przykład 2-1-0 zamiast 0-1-2), ale zasada pozostaje ta sama.

Dzieje się przed regułami

Ostatnią rzeczą, którą dzisiaj poruszymy, jest koncepcja „ dzieje się wcześniej ”. Jak już wiesz, w Javie harmonogram wątków wykonuje większość pracy związanej z przydzielaniem czasu i zasobów wątkom w celu wykonania ich zadań. Wielokrotnie widziałeś również, jak wątki są wykonywane w losowej kolejności, której zwykle nie da się przewidzieć. Ogólnie rzecz biorąc, po programowaniu „sekwencyjnym”, które wykonaliśmy wcześniej, programowanie wielowątkowe wygląda jak coś losowego. Uwierzyłeś już, że możesz użyć wielu metod do kontrolowania przepływu programu wielowątkowego. Ale wielowątkowość w Javie ma jeszcze jeden filar — 4 reguły „dzieje się przed ”. Zrozumienie tych zasad jest dość proste. Wyobraź sobie, że mamy dwa wątki — AiB. Każdy z tych wątków może wykonywać operacje 1i 2. W każdej regule, kiedy mówimy „ A dzieje się przed B ”, mamy na myśli, że wszystkie zmiany dokonane przez wątek Aprzed operacją 1oraz zmiany wynikające z tej operacji są widoczne dla wątku Bpodczas 2wykonywania operacji i później. Każda reguła gwarantuje, że podczas pisania programu wielowątkowego pewne zdarzenia wystąpią przed innymi w 100% przypadków i że w czasie działania 2wątek Bbędzie zawsze świadomy zmian, które wątek Awprowadził podczas działania 1. Przejrzyjmy je.

Zasada nr 1.

Zwolnienie muteksu następuje, zanim ten sam monitor zostanie przejęty przez inny wątek. Myślę, że tutaj wszystko rozumiesz. Jeśli mutex obiektu lub klasy jest pozyskiwany przez jeden wątek, na przykład przez thread A, inny wątek (thread B) nie może go uzyskać w tym samym czasie. Musi czekać, aż muteks zostanie zwolniony.

Zasada 2.

Metoda dzieje Thread.start()się wcześniej Thread.run() . Ponownie, nic trudnego tutaj. Wiesz już, że aby uruchomić kod wewnątrz run()metody, musisz wywołać start()metodę w wątku. W szczególności metoda startowa, a nie run()sama metoda! Ta reguła zapewnia, że ​​wartości wszystkich zmiennych ustawionych przed Thread.start()wywołaniem będą widoczne wewnątrz run()metody po jej uruchomieniu.

Zasada 3.

Koniec metody run()następuje przed powrotem z join()metody. Wróćmy do naszych dwóch wątków: Ai B. Wywołujemy join()metodę, aby wątek Bmiał gwarancję oczekiwania na zakończenie wątku, Azanim wykona swoją pracę. Oznacza to, że metoda obiektu A run()ma gwarancję działania do samego końca. A wszystkie zmiany danych, które mają miejsce w run()metodzie wątku, Amają stuprocentową gwarancję, że będą widoczne w wątku Bpo ich zakończeniu, czekając, aż wątek Azakończy swoją pracę, aby mógł rozpocząć własną pracę.

Zasada 4.

Zapis do volatilezmiennej odbywa się przed odczytem z tej samej zmiennej. Kiedy używamy volatilesłowa kluczowego, faktycznie zawsze otrzymujemy bieżącą wartość. Nawet z longlub double(mówiliśmy wcześniej o problemach, które mogą się tutaj zdarzyć). Jak już rozumiesz, 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, w których takie zachowanie nam nie odpowiada. Załóżmy, że przypisujemy wartość do zmiennej w wątku A:

int z;

….

z = 555;
Gdyby nasz Bwątek miał wyświetlać wartość zmiennej zna konsoli, mógłby bez problemu wyświetlić 0, ponieważ nie wie o przypisanej wartości. Ale Reguła 4 gwarantuje, że jeśli zadeklarujemy zmienną zjako volatile, to zmiany jej wartości w jednym wątku będą zawsze widoczne w innym wątku. Jeśli dodamy do słowa volatiledo poprzedniego kodu...

volatile int z;

….

z = 555;
...wtedy zapobiegamy sytuacji, w której wątek Bmógłby wyświetlić 0. Zapis do volatilezmiennych następuje przed odczytem z nich.
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION