„Hallo Amigo! Du erinnerst dich sicher, dass Ellie dir von den Problemen erzählt hat, die entstehen, wenn mehrere Threads gleichzeitig versuchen, auf eine gemeinsame Ressource zuzugreifen, oder?

„Ja.“

„Nun, das ist noch nicht alles. Es gibt noch ein weiteres kleines Problem.“

Wie du weißt, hat ein Computer einen Speicher, in dem Daten und Befehle (Code) gespeichert werden, sowie einen Prozessor, der diese Befehle ausführt und mit den Daten arbeitet. Der Prozessor liest die Daten aus dem Speicher, verändert sie und schreibt sie wieder zurück in den Speicher. Um die Berechnungen zu beschleunigen, hat der Prozessor einen eigenen „schnellen“ Speicher integriert: den Cache.

Der Prozessor läuft schneller, indem er die am häufigsten verwendeten Variablen und Speicherbereiche in seinen Cache kopiert. Dann nimmt er alle Änderungen in diesem schnellen Speicher vor. Und dann kopiert er die Daten zurück in den „langsamen“ Speicher. Währenddessen enthält der langsame Speicher die alten (unveränderten!) Variablen.

Hier liegt das Problem. Ein Thread ändert eine Variable, wie isCancel oder isInterrupted im obigen Beispiel, aber ein zweiter Thread sieht diese Änderung nicht, weil sie im schnellen Speicher erfolgt ist. Das liegt daran, dass die Threads keinen Zugriff auf den Cache der anderen Threads haben. (Ein Prozessor enthält oft mehrere unabhängige Kerne und die Threads können auf physikalisch unterschiedlichen Kernen laufen.)

Erinnern wir uns an das Beispiel von gestern:

Code Beschreibung
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("Tick");
}
}
}
Der Thread „weiß nicht“, dass die anderen Threads existieren.

In der run-Methode wird die Variable isCancel bei der ersten Verwendung in den Cache des untergeordneten Threads gelegt. Dieser Vorgang entspricht dem folgenden Code:

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

Das Aufrufen der cancel-Methode von einem anderen Thread aus verändert den Wert von isCancel im normalen (langsamen) Speicher, aber nicht in den Caches anderer Threads.

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

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

„Oh! Haben sie denn dafür auch eine so schöne Lösung gefunden, wie bei synchronized?“

„Du wirst es nicht glauben!“

Die erste Idee war, den Cache zu deaktivieren, aber dadurch liefen die Programme um ein Vielfaches langsamer. Dann entstand eine andere Lösung.

Das Schlüsselwort volatile wurde geboren. Wir setzen dieses Schlüsselwort vor eine Variablendeklaration, um anzuzeigen, dass ihr Wert nicht in den Cache gelegt werden darf. Genauer gesagt ist es nicht so, dass sie nicht in den Cache gelegt werden konnte, sondern es war einfach so, dass sie immer aus dem normalen (langsamen) Speicher gelesen und in ihn geschrieben werden musste.

Hier siehst du, wie du unsere Lösung so korrigierst, dass alles einwandfrei funktioniert:

Code Beschreibung
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("Tick");
}
}
}
Der volatile-Modifikator bewirkt, dass eine Variable immer aus dem normalen, von allen Threads gemeinsam genutzten Speicher gelesen und in diesen geschrieben wird.
public static void main(String[] args)
{
Clock clock = new Clock();
Thread clockThread = new Thread(clock);
clockThread.start();

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

„Das war‘s?“

„Das war‘s. Einfach und schön.“