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 pamiętają ci, którzy czytali samą teorię, wątek w Java musi być opatrzony serią niezbędnych zabezpieczeń celem eliminacji wszelkich kolizji. Tematem na dziś będzie spójność danych, czyli jak sprawić, żeby modyfikacja pewnej danej przez jeden wątek obowiązywała w innych obecnie działających wątkach. Java dysponuje pewnymi sposobami wdrażania takiego zabezpieczenia.

WĄTEK W JAVA. ZAGROŻENIE PRZY MODYFIKACJI DANYCH

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 exception)
		{
			exception.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 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ć.

TERMIN "ATOMOWOŚĆ" KLUCZEM DO SUKCESU

Wątek w 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. 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.

Operacja atomowa - wątek w Java

Atomowość (niepodzielność) polega na potraktowaniu wielu pojedynczych poleceń jako jedno WIELKIE polecenie, które albo ma się zakończyć pomyślnie od początku do końca, albo w ogóle nie wykonać żadnych poleceń. 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 w Java nie dostał się do tej metody i teraz tego nie robi. Naszym zadaniem jest sprawienie, żeby wykonana została modyfikacja atomowo, czyli żeby tylko jeden wątek mógł to zrobić w danym czasie i dopiero wtedy, gdy skończy, inny wątek mógł to wykonać.

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óra ma być niepodzielna (atomowa). 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 w Java 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
}

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ć. Dziękuję za dotrwanie do samego końca i nie plujcie sobie w brodę jeśli do tej pory tego nie rozumiecie.

PODOBNE ARTYKUŁY