Synchronizacja wątków

Sytuacja, w której wiele wątków modyfikuje i odczytuje współbieżnie stan tych samych, współdzielonych obiektów, może doprowadzić do wystąpienia sytuacji niepożądanych, wynikających z braku determinizmu co do kolejności wykonania instrukcji poszczególnych wątków.

Aby umożliwić rozwiązywanie problemów tego typu język Java udostępnia mechanizm synchronizacji fragmentów kodu, który to mechanizm pozwala zagwarantować spójność wykonania wybranego ciągu instrukcji.

Przypomnijmy sobie, z czego wynikał problem nieprzewidywalności wyniku działania programu TestClassA, opisany w poprzednim odcinku kursu, odcinku „Wykonanie wielowątkowe”. Otóż problem ten wynikał z faktu, że operacja val = val + 1 nie jest z punktu widzenia Wirtualnej Maszyny Javy pojedynczą instrukcją, lecz ciągiem kilku niezależnych operacji.

Problem niepoprawnej współbieżnej modyfikacji wartości zmiennej val moglibyśmy zatem rozwiązać, gdybyśmy mogli zapewnić, że instrukcja val = val + 1 wykonywana będzie zawsze w całości, tzn. jeśli jeden wątek rozpocznie wykonywanie tej instrukcji, to żaden inny nie będzie mógł rozpocząć, dopóki on nie skończy. Właśnie to za chwilkę zrobimy, ale w tym celu musimy zrozumieć jeszcze jeden aspekt synchronizacji.

Wyobraźmy sobie, że w przeddzień wyborów zorganizowano debatę. Zaproszono kilku polityków. Debatę prowadzi redaktor. Politycy, jak to politycy – każdy chce mówić jak najwięcej, więc w rezultacie wszyscy mówią na raz. Wiele usłyszeć się nie da, a jeszcze mniej można zrozumieć. Wyobraźmy sobie, że mądry redaktor w pewnym momencie przerywa i ustala prostą zasadę – mówić w danej chwili może tylko ta osoba, która trzyma w rękach długopis. Przypuśćmy, że wszyscy przystają na ten układ. Redaktor wręcza więc długopis jednemu z polityków; ten mówi i mówi, aż po chwili ten sam redaktor długopis mu zabiera i daje go następnej osobie. Teraz mówi ten drugi. Oczywiście aby mechanizm funkcjonował, w obiegu musi znajdować się jeden długopis. W tym przypadku jest to ten konkretny długopis, który redaktor wręczył na początku jednemu z polityków. Mechanizm jest bardzo prosty i skuteczny, oto bowiem mówić może tylko jedna osoba na raz.

Przejdźmy teraz od historyjki do Javy. Tak jak w przypadku polityków pożądane było, aby nie dochodziło do sytuacji w której kilku mówi na raz, i w której jeden przerywa drugiemu w połowie zdania, tak w Javie chodzi o to, aby wybrany ciąg instrukcji mógł być bezpiecznie wykonany, przez jeden wątek na raz i od początku do końca.

Podobnie jak w przypadku polityków funkcjonował długopis, który rozstrzygał o tym który polityk może mówić, tak i w Javie mamy takiego strażnika. Określamy go każdorazowo, gdy wskazujemy fragment kodu który należy synchronizować. Tak jak w przypadku debaty obiektem użytym do synchronizacji mógł być dowolny przedmiot, tak w Javie możemy do synchronizacji użyć dowolnego obiektu.

Wróćmy jeszcze na chwilę do debaty politycznej. Nic nie stoi na przeszkodzie, aby w tym samym czasie zorganizować kilka debat. Jedna może być polityczna, a jakaś inna, mająca miejsce gdzie indziej, ale w tym samym czasie, merytoryczna. Tak jak nie chcemy by w trakcie jednej debaty kilku gadało na raz, tak nic nam nie przeszkadza, jeśli na jednej debacie ktoś będzie mówił w tym samym czasie co ktoś drugi na debacie drugiej. Jeden drugiemu nie przeszkadza, bo mówią o czym innym i gdzie indziej.

Analogicznie jest w Javie. Jeśli mamy dwie grupy wątków i wątki z obydwu grup modyfikują stan innych obiektów, to również nic nie stoi na przeszkodzie, aby robiły to współbieżnie. Ważne jest, żeby zrozumieć, że ilekroć synchronizujemy wątki, to synchronizujemy je zawsze na jakimś obiekcie. Ten obiekt odpowiada długopisowi synchronizującemu debatę polityczną, i tak jak powinniśmy używać różnych długopisów do synchronizacji niezależnych debat, tak powinniśmy używać różnych obiektów do synchronizacji kodu operującego na niezależnych obiektach.

Fragment kodu synchronizujemy umieszczając go w obrębie bloku synchronized, zgodnie ze składnią pokazaną poniżej:

synchronized ({obiekt synchronizujący}) {
  {kod synchronizowany}
}

Element {obiekt synchronizujący} to referencja do obiektu, na którym synchronizowany będzie kod {kod synchronizowany}.

Aby zsynchronizować wszystkie wątki konkurujące o możliwość wykonania instrukcji val = val + 1 z metody run() klasy MyRunnable moglibyśmy więc napisać:

class MyRunnable implements Runnable {
  int val = 0;
  
  public void run() {
    synchronized(this) {
      val = val + 1;
    }
  
    System.out.println(val);
  }
}

Tak jak debatę polityczną synchronizujemy na przedmiocie związanym ściśle z tą debatą, tak operacje wykonywane na zmiennej val musimy synchronizować na obiekcie związanym ściśle z tą zmienną. Najprościej byłoby te operacje synchronizować na samej zmiennej val, ale jest ona typu prostego, więc się nie da. Synchronizujemy zatem na obiekcie który tę zmienną zawiera. Jedno drugiemu ściśle odpowiada (jedno drugie zawiera), więc wychodzi na to samo.

Zauważmy, że zapewniliśmy nierozerwalność operacji val = val + 1, jednak wyświetlenie wartości tej zmiennej nadal jest instrukcją niezależną. Nadal możliwa jest sytuacja, w której każdy z trzech wątków wykona wpierw instrukcję val = val + 1 (tym razem już w całości) a dopiero potem każdy z nich wyświetli wartość zmiennej val. Mamy więc gwarancję co do tego, że zmienna val na końcu będzie miała wartość 3, jednak nadal możliwe jest, że na ekranie wyświetli się ciąg cyfr 3, 3, 3. Aby i ten problem wyeliminować należałoby włączyć instrukcję System.out.println(val) do wnętrza bloku synchronized.

Materiał tego kursu nie wyczerpuje w pełni tematyki programowania współbieżnego w Javie, ale mamy już solidne podstawy. Jeśli udało nam się zrozumieć wszystko to, co zostało w tym względzie napisane, to rozumiemy już wszystko to, co w tym względzie najważniejsze.

0 0 votes
Daj ocenę
Subscribe
Powiadom o
guest

1 Komentarz
najstarszy
najnowszy oceniany
Inline Feedbacks
View all comments