Dobra. Dotychczas temat wątków opierał się o same podstawowe zagadnienia. Uprzedzam, że od tej pory będziemy wchodzić w szczegóły, które mogą spowodować zrobienie sobie dłuższego "przystanku". Jak dobrze wiedzą ci, którzy czytali samą teorię, wątek w języku Java musi być opatrzony serią niezbędnych zabezpieczeń celem eliminacji wszelkich kolizji. Tematem na dziś będzie spójność danych, czyli jak sprawić żeby przy pomocy słowa kluczowego "synchronized" w języku Java, modyfikacja pewnej danej przez jeden wątek obowiązywała w innych obecnie działających wątkach. Język dysponuje pewnymi sposobami wdrażania takiego zabezpieczenia i dzisiaj poznamy jeden z nich.
Tweet |
ZACZNIEMY OD KOŃCA
Zacznę wpierw od zaprezentowania takiej sytuacji, abyście zdali sobie sprawę, że to nie jest "pitu-pitu" obok którego można sobie przejść obojętnie i machnąć na to ręką. Wklejcie sobie poniższe kody źródłowe, skompilujcie sobie program i go uruchomcie. Uważnie obserwować wyniki:
KLASA "MAIN"
public class Main {
public static void main(String[] args) {
new Launcher();
}
}
KLASA "VALUECONTAINER"
public class ValueContainer {
private int v = 0;
@Override
public String toString() {
return "Wartość v = " + v;
}
public void increment(String threadName) {
++v;
System.out.println(threadName + " - " + this);
}
}
KLASA "VALUETHREAD"
public class ValueThread extends Thread {
private final ValueContainer valueContainer;
public ValueThread(ValueContainer vc) {
valueContainer = vc;
}
@Override
public void run() {
for (int i = 1; i <= 10; ++i) {
performTask();
}
System.out.println("Końcowa wartość: " + valueContainer);
}
private void performTask() {
try {
Thread.sleep(200);
}
catch (Exception e) {
e.printStackTrace();
}
valueContainer.increment(getName());
}
}
KLASA "LAUNCHER"
public class Launcher {
public Launcher() {
ValueContainer vc = new ValueContainer();
for (int t = 1; t <= 3; ++t) {
ValueThread vt = new ValueThread(vc);
vt.setName("Wątek #" + t);
vt.start();
}
}
}
Odpalając program interesuje nas wartość liczbowa na samym końcu. Zmienna "v" będzie przyjmować różne wartości przez co będzie "skakać" pomiędzy jedną liczbą, a drugą nie zachowując przy tym spójności danych. Wypis komunikatu na samym końcu ze strony każdego wątku będzie świadczył jednoznacznie, że wartość nie jest prawidłowa (powinna być równa liczbie uruchomionych wątków przemnożonej przez liczbę iteracji w pętli "for" na pojedynczy wątek w metodzie "run"). Zastosowałem specjalnie usypianie wątku, aby sprowokować zjawisko utraty pewnych modyfikacji liczby. Bez metody "sleep", procesor mógłby na tyle szybko wykonać zadania, że omawiana teraz pułapka mogłaby się nigdy nie ukazać (co nie znaczy, że nie byłoby jej!).
TERMIN "ATOMOWOŚĆ" KLUCZEM DO SUKCESU
Wątek w języku Java nie obchodzi czy jakiś inny wątek w chwili obecnej również modyfikuje tę samą daną. Ale nas musi obchodzić, że wątki te jeśli już oddziałują na tę samą zmienną, to ma ulegać ona modyfikacjom ze strony każdego wątku! W tym miejscu dotykamy bardzo istotnego pojęcia dotyczącego modyfikatora "synchronized" w języku Java. Jest nim "atomowość".
Mówiąc o operacji że jest atomowa, oznacza to tyle że pewna sekwencja kilku poleceń w jednym miejscu MUSI być potraktowana jako jeden ciąg, który kończy się sukcesem od początku do samego końca. Innymi słowy, taka operacja nie może zostać wykonana jedynie w pewnej części. Ona ma przyjmować tylko jeden z dwóch stanów: albo została ona rozpoczęta i zakończyła się pomyślnie w pełni, albo w ogóle nie została ona przeprowadzona.
Atomowość (niepodzielność) polega na potraktowaniu wielu pojedynczych poleceń jako jedno WIELKIE polecenie, które albo ma się wykonać od początku do końca, albo w ogóle się nie wykonać. Nie ma żadnych półśrodków!
W naszym przykładzie problem polega na tym, że dochodzi do kilku preinkrementacji naraz przeprowadzanych niezależnie od tego, czy już jakiś wątek nie dostał się do tej metody i teraz tego nie robi w tym samym czasie. Naszym zadaniem jest sprawienie, żeby modyfikacja została wykonana "atomowo", czyli żeby tylko jeden wątek mógł to zrobić w danym czasie i dopiero wtedy, gdy skończy, żeby inny wątek mógł to wykonać.
POZNAJ SŁOWO "SYNCHRONIZED" W JĘZYKU JAVA
Java udostępnia kilka sposobów zapewnienia spójności danych, a na tę chwilę zaprezentuję tylko jedną, aby nie komplikować. Najprostszą metodą jest wprowadzenie słowa kluczowego "synchronized" do nagłówka metody, której instrukcje mają być niepodzielne (atomowe). Druga rzecz to zastanowienie się w którym miejscu to słowo wprowadzić. Problem robi się przy dodawaniu do licznika, zatem metoda "increment" musi mieć nałożoną blokadę. Poprawna definicja metody synchronizowanej wygląda tak:
public synchronized void increment(String threadName) {
++v;
System.out.println(threadName + " - " + this);
}
Teraz sytuacja ulegnie diametralnej zmianie i w ten sposób wątek wchodząc do metody synchronizowanej, będzie brał "klucz" i nie puści go do momentu, dopóki nie zakończy WSZYSTKICH zawartych tam poleceń, bez względu na to, czy zostanie w trakcie uśpiony, czy też nie. Dzięki temu, inkrementacja będzie wykonywana atomowo, a co za tym idzie, każdy wątek będzie co każdą iterację dodawał do licznika jedynkę i ta modyfikacja się utrwali.
Można też osadzić sam blok kodu, który ma być potraktowany atomowo, czyli żeby jedynie część danej metody była niepodzielna. Równie dobrze można nasz problem rozwiązać tak:
public void increment(String threadName) {
synchronized(this) {
++v;
System.out.println(threadName + " - " + this);
}
// reszta poleceń, która może być bezpiecznie wykonywana fragmentarycznie
}
To sprawi, że tylko dwie pierwsze instrukcje będą atomowe, a reszta będzie wykonywana tradycyjnie. "this" oznacza nałożenie blokady wyłącznie na tę klasę.
Na sam koniec pragnę dodać, żeby oszczędnie stosować synchronizację tylko tam, gdzie to jest NAPRAWDĘ konieczne. Dodanie takiego "zamka" do metody obciąża nieco efektywność programu i można go sobie trochę spowolnić (tak twierdzi książka, którą miałem okazję przeczytać). Dziękuję za dotrwanie do samego końca i nie plujcie sobie w brodę, jeśli do tej pory tego nie rozumiecie.