|
|||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||
|
Atomic Scalars
|
||||||||||||||||||||||||||||
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ählerLeider 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 !!!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 CAS - Compare-and-SwapProzessoren 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:
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 VariablenKehren 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 optimiertDer 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. ZusammenfassungIn 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.LiteraturverweiseDie gesamte Serie über das Java Memory Model:
|
|||||||||||||||||||||||||||||
© 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 |