Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | Twitter | Lanyrd | Linkedin
 
HOME 

  OVERVIEW

  BY TOPIC
    JAVA
    C++

  BY COLUMN
    EFFECTIVE JAVA
    EFFECTIVE STDLIB

  BY MAGAZINE
    JAVA MAGAZIN
    JAVA SPEKTRUM
    JAVA WORLD
    JAVA SOLUTIONS
    JAVA PRO
    C++ REPORT
    CUJ
    OTHER
 

GENERICS 
LAMBDAS 
IOSTREAMS 
ABOUT 
CONTACT 
Atomic Scalars

Atomic Scalars
Java Memory Model
Atomic Scalars

Java Magazin, August 2009
Klaus Kreft & Angelika Langer

Dies ist das Manuskript eines Artikels, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im Java Magazin erschienen ist.  Die übrigen Artikel dieser Serie sind ebenfalls verfügbar ( click here ).
 
Die ganze Serie zum  "Java Memory Modell" als PDF (985 KB).
 

Wir haben in einem der Beiträge in dieser Kolumne [ JMM4 ] über die Performance-Vorzüge und Speichereffekte von volatile im Kontext von konkurrierenden Threads geschrieben. Leider hat volatile gewisse Einschränkungen.  So ist es beispielsweise nicht möglich, eine atomare Read-Modify-Write-Sequenz auf einer volatile Variablen auszuführen; es sind nur die einzelnen Lese- und Schreibzugriffe atomar, nicht aber die ganze Sequenz.  Als logische Verallgemeinerung von volatile Variablen  gibt es seit Java 5.0 atomare Variablen, die ununterbrechbare Read-Modify-Write-Sequenzen unterstützen und ansonsten dieselben Vorzüge und Speichereffekte wie volatile haben.   In diesem Beitrag sehen wir uns die atomaren skalaren Variablen an.

Es gibt immer mal wieder Situationen, in denen man die Thread-Synchronisation mit Hilfe von Locks durch die Verwendung von volatile-Variablen ersetzt möchte, um die Performance zu verbessern.  Das ist möglich, wenn der Zugriff auf eine gemeinsam verwendete Variable sowieso schon ununterbrechbar ist, wie zum Beispiel beim lesenden oder schreibenden Zugriff auf primitive Typen (außer long und double) und Referenzen.  In solchen Fällen wird die Synchronisation nicht gebraucht, um den Zugriff auf die Variable ununterbrechbar zu machen, sondern nur noch, um die Variable sichtbar zu machen.  Da es seit Java 5.0 aber Sichtbarkeitsgarantien für volatile-Variablen gibt, kann man hier auf die Synchronisation verzichten und stattdessen volatile verwenden. Mit long und double funktioniert die Lösung auch.  Im Unterschied zu den übrigen primitiven Typen ist der Zugriff auf non-volatile-Variablen vom Typ long und double unterbrechbar, aber wenn eine Variablen vom Typ long und double als volatile erklärt ist, dann ist der Zugriff atomar. Diese Ununterbrechbarkeit ist bei long und double also ein zusätzlicher Effekt von volatile, den man bei dieser Situation ausnutzt.

Ein threadsicherer Zähler

Leider ist bei einer solchen Optimierung mit volatile nur der einzelne Lese- bzw. Schreibzugriff atomar. Dessen sollte man sich bewusst sein, sonst ist die Optimierung auf einmal gar nicht mehr threadsicher.  Hier ist ein Beispiel für eine Abstraktion, in der aus Optimierungsgründen auf Synchronisation verzichtet wird und stattdessen volatile verwendet wird:
public class VolatileCounter {  // Vorsicht: falsch !!!
  private volatile int value;
  public int getValue()  { return   value; }
  public int increment() { return ++value; }
  public int decrement() { return --value; }
}
Leider hat sich ein Fehler eingeschlichen.  Der völlige Verzicht auf Synchronisation ist hier nicht korrekt, denn nicht alle Zugriffe auf das int-Feld value sind ununterbrechbar.  Die Sprachspezifikation garantiert zwar, dass der lesende und schreibende Zugriff auf Variablen von primitivem Typ ununterbrechbar ist, wenn sie als volatile deklariert sind. Wie oben bereits erwähnt, gilt das auch für volatile-Variablen vom Typ long und double.  Für das Inkrementieren und Dekrementieren gilt es aber nicht.  Ein Inkrement ist eine Sequenz von Lesen, Modifizieren und Schreiben.  Das ist bereits eine so komplexe Operation, dass sie unterbrechbar ist.

Man muss also die Methoden increment() und decrement() synchronisieren, damit sie ununterbrechbar sind.  Lediglich die Methode getValue() kann ohne Synchronisation auskommen.  Dann sähe die Counter-Klasse so aus:

public class SynchronizedCounter {  // korrekt, aber nicht optimal
  private volatile int value;
  public int getValue()  { return   value; }
  public synchronized int increment() { return ++value; }
  public synchronized int decrement() { return --value; }
}

CAS - Compare-and-Swap

Prozessoren haben im allgemeinen ein atomaren Befehl, der eine sogenannte atomare Compare-and-Swap-Operation (auch CAS genannt) ausführt. Basierend auf solchen atomaren CAS-Operationen der Hardware kann man ununterbrechbare Read-Modify-Write-Sequenzen durchführen, die ganz ohne Synchronisation auskommen.

Leider war es bis Java 5.0 nicht möglich, diese Funktionalität von Java einfach auszunutzen.  Seit Java 5.0 gibt es im Package java.util.concurrent.atomic verschiedene Klassen, die basierend auf CAS-Operationen ununterbrechbare Read-Modify-Write-Sequenzen anbieten.  Dazu gehören Typen, wie AtomicInteger, AtomicLong und AtomicBoolean.

Eine Compare-and-Swap-Operation hat 3 Operanden:

  • eine Speicherstelle
  • den erwarteten alten Wert an dieser Speicherstelle
  • einen neuen Wert für die Speicherstelle
Der Prozessor liest den alten Wert, vergleicht ihn mit dem erwarteten Wert und schreibt den neuen Wert an die Speicherstelle, wenn der gelesene Inhalt dem erwarteten Wert entspricht; wenn die Erwartung nicht zutrifft, wird nichts geändert.  Die ganze Sequenz von Lesen, Vergleichen und Ändern ist ununterbrechbar.

Damit kann man ohne Synchronisation Modifikationen an Variablen vornehmen, die von mehreren Thread gemeinsam verwendet werden.  Das Vorgehen ist dabei folgendes: Man verfolgt eine optimistische Strategie und versucht einfach mal, die Variable zu ändern.  Wenn kein anderer Thread konkurrierend zugreift, dann klappt die CAS-Operation.  Wenn sie scheitert, dann gab es wohl konkurrierende Zugriffe und ein anderer Thread war schneller.  In diesem Fall wird der Änderungsversuch solange wiederholt, bis er erfolgreich ist.

Solche atomaren CAS-Operationen haben den Vorteil, dass sie bei wenig Konkurrenz um den Variablenzugriff performanter sind als synchronisierte Zugriffe, weil der gesamte Synchronisationsaufwand wegfällt.  Bei sehr viel Konkurrenz kann unter Umständen die Synchronisation wieder günstiger sein, weil die CAS-Zugriffe dann mit hoher Wahrscheinlicheit scheitern und wiederholt werden müssen.  Das kostet dann zusätzliche CPU-Zeit.  Atomare Operationen haben aber gegenüber Synchronisation stets den Vorteil, dass zumindest ein Thread immer erfolgreich sind wird.  So etwas wie Deadlocks, wo kein einziger Thread mehr arbeitet, kann mit atomaren Operationen nicht vorkommen.

Neben der Ununterbrechbarkeit haben die Zugriffe auf atomare Variablen dieselben Speichereffekte wie die Zugriffe auf volatile-Variablen.  Deshalb werden die atomaren Variable auch als logische Verallgemeinerung von volatile angesehen.

Ein threadsicherer Zähler unter Verwendung von atomaren Variablen

Kehren wir zu unserem threadsicheren Zähler von oben zurück.  Wie würde er aussehen, wenn wir ihn mit einer atomaren Variablen aus dem Package java.util.concurrent.atomic implementieren?  Offensichtlich bietet sich hier die Verwendung des AtomicInteger an.  Damit sieht die Counter-Abstraktion dann so aus:
public class AtomicCounter {  // korrekt und optimiert
  private AtomicInteger value = new AtomicInteger();
  public int getValue() { return value.get(); }
  public int increment() {
   int oldValue = value.get();
   while (!value. compareAndSet (oldValue, oldValue + 1))
     oldValue = value.get();
   return oldValue + 1;
  }
  public int decrement() {
   int oldValue = value.get();
   while (!value. compareAndSet (oldValue, oldValue - 1))
     oldValue = value.get();
   return oldValue - 1;
  }
}
Der Zähler ist jetzt kein int mehr, sondern ein AtomicInteger.  Die Methode getValue() liefert in der atomaren Operation AtomicInteger.get() den Wert des Integers.  Da atomare Variablen diesselben Speichereffekte wie volatile haben, ist das Lesen mit einem Refresh verbunden.

Das Inkrementieren erfolgt nun so:  zunächst wird der aktuelle Wert des Integers gelesen, um den neuen Wert oldValue+1 zu berechnen.  Dann soll die Modifikation gemacht werden.  Zwischen Lesen und Modifizieren ist der Thread unterbrechbar; es könnte also ein anderer Thread den Integer in der Zwischenzeit modifiziert haben.  Deshalb wird die atomare Operation compareAndSet() aufgerufen.  Sie prüft, ob der zuvor gelesene Wert noch aktuell ist.  Wenn ja, dann ist die Modifikation erfolgreich und es kommt true zurück.  Wenn nein, dann kommt false zurück und der ganze Vorgang von "Wert auslesen", "neuen Wert berechnen" und compareAndSet() wird wiederholt.  Da compareAndSet() mit Lesen und Schreiben der Variablen verbunden ist, wird sowohl ein Refresh als auch ein Flush ausgelöst.  Das Dekrementieren funktioniert analog.

Typisch für die Verwendung von atomaren Variablen ist die Schleife, in der die Operation solange wiederholt wird, bis sie erfolgreich ist.  Insgesamt sieht die Counter-Implementierung mit Hilfe von atomaren Variablen deutlich komplizierter aus als die einfache Implementierung mit Synchronisation.  Die Klasse AtomicInteger hat zwar spezielle incrementAndGet- und getAndIncrement-Methoden, die dem Postfix- und Prefix-Inkrement entsprechen; analog fürs Dekrementieren.  Deutlich einfacher sieht die Lösung damit aber auch nicht aus.  Zum anderen wollten wir aus didaktischen Gründen hier auch das typische Vorgehen bei der Benutzung von atomaren Variablen zeigen.

Zusammenfassung

In diesem Beitrag haben wir uns angesehen, wie man atomare Skalare aus dem Package java.util.concurrent.atomic als bessere volatile Variablen für die Optimierung von konkurrierenden Zugriffen auf skalare Variablen einsetzen kann.  Von zentraler Bedeutung sind dabei die sogenannten CAS(Compare-and-Swap)-Sequenzen, die im allgemeinen von der Hardware als ununterbrechbare Operationen unterstützt werden und über Java-Abstraktionen wie AtomicInteger, AtomicLong und AtomicBoolean nutzbar gemacht werden.  Neben den atomare skalaren Variablen gibt es auch noch atomare Referenzvariablen.  Die besprechen wir im nächsten Beitrag.

Literaturverweise

Die gesamte Serie über das Java Memory Model:

/JMM1/ Einführung in das Java Memory Model: Wozu braucht man volatile?
Klaus Kreft & Angelika Langer, Java Magazin, Juli 2008
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/37.JMM-Introduction/37.JMM-Introduction.html
/JMM2/ Überblick über das Java Memory Model
Klaus Kreft & Angelika Langer, Java Magazin, August 2008
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/38.JMM-Overview/38.JMM-Overview.html
/JMM3/ Die Kosten der Synchronisation
Klaus Kreft & Angelika Langer, Java Magazin, September 2008
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/39.JMM-CostOfSynchronization/39.JMM-CostOfSynchronization.html
/JMM4/ Details zu volatile-Variablen
Klaus Kreft & Angelika Langer, Java Magazin, Oktober 2008
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/40.JMM-volatileDetails/40.JMM-volatileDetails.html
/JMM5/ volatile und das Double-Check-Idiom
Klaus Kreft & Angelika Langer, Java Magazin, November 2008
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/41.JMM-DoubleCheck/41.JMM-DoubleCheck.html
/JMM6/ Regeln für die Verwendung von volatile
Klaus Kreft & Angelika Langer, Java Magazin, Dezember 2008
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/42.JMM-volatileIdioms/42.JMM-volatileIdioms.html
/JMM7/ Die Initialisation-Safety-Garantie für final-Felder von primitivem Typ
Klaus Kreft & Angelika Langer, Java Magazin, Februar 2009
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/43.JMM-InitializationSafety.1/43.JMM-InitializationSafety.1.html
/JMM8/ Die Initialisation-Safety-Garantie für final-Felder von einem Referenztyp
Klaus Kreft & Angelika Langer, Java Magazin, April 2009
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/44.JMM-InitializationSafety.2/44.JMM-InitializationSafety.2.htm l
/JMM9/ Über die Gefahren allzu aggressiver Optimierungen
Klaus Kreft & Angelika Langer, Java Magazin, Juni 2009
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/45.JMM-AggressiveOpt/45.JMM-AggressiveOpt.html
/JMM10/ Atomic Scalars
Klaus Kreft & Angelika Langer, Java Magazin, August 2009
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/46.JMM-AtomicScalar/46.JMM-AtomicScalar.html

 
 

If you are interested to hear more about this and related topics you might want to check out the following seminar:
Seminar
 
Concurrent Java - An in-depth seminar covering all that is worth knowing about concurrent programming in Java, from basics such as synchronization over the Java 5.0 concurrency utilities to the intricacies of the Java Memory Model (JMM).
4 day seminar ( open enrollment and on-site)
 

 
  © Copyright 1995-2015 by Angelika Langer.  All Rights Reserved.    URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/46.JMM-AtomicScalar/46.JMM-AtomicScalar.html  last update: 22 Mar 2015