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ątkiemlong
i double
są 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 long
i . double
Dlaczego? 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. long
oraz 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:
- Zawsze będzie odczytywany i zapisywany atomowo. Nawet jeśli jest to wersja 64-bitowa
double
lublong
. - Maszyna Java nie będzie go buforować. Wykluczona jest zatem sytuacja, gdy 10 wątków działa na swoich lokalnych kopiach.
metoda wydajności().
Przyjrzeliśmy się już wielu metodom tej klasyThread
, 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! Kiedy 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-1
i Thread-2
. Thread-0
zaczyna się jako pierwszy i natychmiast ustępuje miejsca innym. Po tym zaczyna się Thread-1
i również ustępuje. Potem się zaczyna Thread-2
, co również jest gorsze. Nie mamy już żadnych wątków, a kiedy Thread-2
ostatni 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-1
wykonuje 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 -A
i B
. Każdy z tych wątków może wykonywać operacje 1
i 2
. A kiedy w każdej z reguł mówimy „ A dzieje się przed B ”, oznacza to, że wszystkie zmiany dokonane przez wątek A
przed operacją 1
oraz zmiany, które ta operacja pociągała za sobą, są widoczne dla wątku B
w momencie wykonania operacji 2
i 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 B
w czasie operacji 2
bę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.
MetodaThread.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 metodyrun()
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 B
musi poczekać na zakończenie A
zanim 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 A
będą całkowicie widoczne w wątku, B
gdy wątek będzie czekał na zakończenie A
i 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 przypadkulong
i 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 B
miałby wydrukować wartość zmiennej z
na 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ą z
jako 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ń B
wyprowadzi na konsolę wartość 0. Zapisywanie do zmiennych niestabilnych następuje przed ich odczytaniem.
GO TO FULL VERSION