– Cześć, Amigo! Pamiętasz, jak Basia opowiadała Ci o problemach, które pojawiają się, gdy kilka wątków próbuje jednocześnie uzyskać dostęp do współdzielonego zasobu?

– Tak.

– Ale to nie wszystko. Jest jeszcze jeden mały problem.

Jak wiesz, komputer ma pamięć, w której przechowywane są dane i komendy (kod), a także procesor, który wykonuje te komendy i z tymi danymi pracuje. Procesor odczytuje dane z pamięci, zmienia je i zapisuje z powrotem do pamięci. Aby przyspieszyć obliczenia, procesor wyposażono we własną, wbudowaną „szybką” pamięć: pamięć podręczną (cache).

Procesor pracuje szybciej, kopiując do swojej pamięci podręcznej najczęściej używane zmienne i obszary pamięci. Następnie dokonuje wszystkich zmian właśnie w tej szybkiej pamięci. A później kopiuje dane z powrotem do pamięci „wolnej”. Przez cały ten czas w wolnej pamięci znajdują się stare (niezmienione!) zmienne.

Tu właśnie pojawia się problem. Jeden wątek zmienia zmienną, tak jak isCancel lub isInterrupted w powyższym przykładzie, ale drugi wątek „nie widzi” tej zmiany, bo nastąpiła ona w szybkiej pamięci. Wynika to z faktu, że wątki nie mają dostępu do swojej pamięci podręcznej. (Procesor często posiada kilka niezależnych rdzeni i wątki mogą być uruchamiane na różnych rdzeniach.)

Przypomnijmy sobie wczorajszy przykład:

Kod Opis
class Clock implements Runnable
{
private boolean isCancel = false;

public void cancel()
{
this.isCancel = true;
}

public void run()
{
while (!this.isCancel)
{
Thread.sleep(1000);
System.out.println("Tik");
}
}
}
Wątek „nie wie”, że istnieją inne wątki.

W metodzie run zmienna isCancel jest umieszczana w pamięci podręcznej wątku podrzędnego, gdy jest używana po raz pierwszy. Operacja ta ma postać poniższego kodu:

public void run()
{
boolean isCancelCached = this.isCancel;
while (!isCancelCached)
{
Thread.sleep(1000);
System.out.println("Tik");
}
}

Wywołanie metody cancel z innego wątku spowoduje zmianę wartości isCancel w normalnej (wolnej) pamięci, ale nie w pamięci podręcznej innych wątków.

public static void main(String[] args)
{
Clock clock = new Clock();
Thread clockThread = new Thread(clock);
clockThread.start();

Thread.sleep(10000);
clock.cancel();
}

– Wow! I czy też znaleziono dla tego tak wspaniałe rozwiązanie, jak w przypadku synchronized?

– Nie uwierzysz!

Pierwszym pomysłem było wyłączenie pamięci podręcznej, ale to sprawiło, że programy działały kilka razy wolniej. Potem znaleziono inne rozwiązanie.

Powstało słowo kluczowe volatile. Umieszczamy to słowo kluczowe przed deklaracją zmiennej, aby wskazać, że jej wartość nie może być umieszczona w pamięci podręcznej. Mówiąc ściślej, nie chodziło o to, że nie można jej umieścić w pamięci podręcznej. Po prostu zawsze trzeba ją było odczytać i zapisać do normalnej (wolnej) pamięci.

Oto jak naprawić nasze rozwiązanie tak, aby wszystko działało bez zarzutu:

Kod Opis
class Clock implements Runnable
{
private volatile boolean isCancel = false;

public void cancel()
{
this.isCancel = true;
}

public void run()
{
while (!this.isCancel)
{
Thread.sleep(1000);
System.out.println("Tik");
}
}
}
Modyfikator volatile sprawia, że zmienna jest zawsze odczytywana i zapisywana do normalnej pamięci współdzielonej przez wszystkie wątki.
public static void main(String[] args)
{
Clock clock = new Clock();
Thread clockThread = new Thread(clock);
clockThread.start();

Thread.sleep(10000);
clock.cancel();
}

– To wszystko?

– To wszystko. Proste i piękne.