9.10 Atomares und frische Werte mit volatile
 
Die JVM arbeitet bei den Ganzzahl-Datentypen kleiner gleich int intern nur mit einem int. Doch obwohl es auch für die 64-Bit Datentypen long und double einzelne Bytecode-Befehle gibt, ist nicht gesichert, dass die Operationen auf diesen Datentypen unteilbar sind, da etwa eine 64-Bit-Zuweisung sich aus zwei 32-Bit Zuweisungen zusammensetzen lässt. Es kann also passieren, dass ein Thread mitten in einer long- oder double-Operation von einem anderen Thread verdrängt wird. Greifen zwei Threads auf die gleiche 64-Bit Variable zu, so könnte es passieren, dass der eine Thread eine Hälfte schreibt und der andere Thread die andere.
Beispiel Zwei Threads versuchen gleichzeitig die Variable l vom Typ long zu ändern. (Aus Gründen der Übersichtlichkeit ist ein Leerzeichen nach dem ersten 32-Bit-Block in der hexadezimalen Notation eingefügt.)
l = 0x00000000 00000000 l = 0xFFFFFFFF FFFFFFFF
Schreibt der erste Thread den ersten Teil der long-Variablen mit 00000000 und findet dann ein Kontextwechsel statt, so dass der zweite Thread die Variable komplett mit FFFFFFFF FFFFFFFF initialisiert, gibt es nach dem erneuten Umschalten ein Problem. Denn dann wird der ersten Thread seine zweite Hälfe schreiben und die Belegung der Variablen ist FFFFFF 00000000.
|
Um das zu vermeiden, können die Objekt- und Klassenvariablen mit dem Modifizierer volatile deklariert werden. Die Zugriffsoperationen werden auf diesen Variablen dann atomar ausgeführt. Der Modifizierer ist bei lokalen Variablen nicht gestattet. Operationen wie i++ sind damit natürlich noch nicht atomar!
Zwischenspeicherung untersagen
volatile beugt zusätzlich einem anderen Problem vor. Während der Berechnung könnte die Laufzeitumgebung Inhalte von Variablen im Prozessorspeicher (zum Beispiel Register) zwischengespeichert haben. Das passiert etwa, wenn in einer Schleife ein Wert immer hoch gezählt wird, der in einer Objektvariablen gespeichert wird.
Eine Laufzeitumgebung könnte den Zugriff auf Objektvariablen optimieren, indem zuerst der Variableninhalt in eine interne lokale Variable kopiert und anschließend, nach einer Berechnung, das Ergebnis zurückgespeichert wird. Ist cnt eine Objektvariable, dann könnte folgende Schleife von der Laufzeitumgebung optimiert werden:
for ( int i = 0; i < 1000000; i++ )
cnt++;
Eine optimierende Laufzeitumgebung könnte nun auf die Idee kommen, nicht bei jedem Schleifendurchlauf die Objektvariable zu erhöhen, sondern nur zum Ende. Wenn die Umgebung zuerst i in eine interne lokale Variable kopiert, dann die gesamte Schleife ausführt und erst anschließend den internen Wert nach i zurückkopiert, haben wir viel Zeit gespart, denn der Zugriff auf eine lokale Variable, die im Register des Prozessors leben kann, ist wesentlich schneller als der Zugriff auf eine Objektvariable. Dies ist im Übrigen eine beliebte Strategie, um die Performanz eines Programms zu steigern. Doch mit dieser internen Optimierung kommt es zu schwer kontrollierbaren Nebeneffekten, und Änderungen am Wert von i sind nicht sofort für andere Threads sichtbar, denen damit die Information über die einzelnen Inkrementschritte fehlt.
Auch hier hilft wieder das ungewöhnliche Schlüsselwort. Ist die Variable mit volatile gekennzeichnet, wird das Ergebnis nicht im Zwischenspeicher belassen, sondern immer aktualisiert. Die parallelen Threads sehen somit immer den korrekten Variablenwert, da er vor jeder Benutzung aus dem Speicher gelesen und nach einer Änderung sofort wieder zurückgeschrieben wird.
9.10.1 Das Paket java.util.concurrent.atomic
 
Während volatile Lesen und Schreiben auf den 64-Bit Datentypen atomar macht, ist eine Operation wie i > 12 mit der Objektvariablen i nicht atomar. Kurz vor dem Vergleich kann ein Thread-Wechsel stattfinden, i geändert werden, so dass der Vergleich einen anderen Ausgang findet. Die Rettung ist ein synchronisierter Block, wobei jedoch eine Synchronisationsvariable nötig ist.
Seit Java 5 helfen hier die Klassen des Pakets java.util.concurrent.atomic. Ein Beispiel für einen veränderbaren Behälter ist AtomicInteger, der Operationen wie getAndIncrement() anbietet, die den Wert des Behälters atomar erhöhen.
|