|
|||||||||||||||||||||||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||||||||||||||||||||||
|
Atomic References
|
||||||||||||||||||||||||||||||||||||||||||||||||
Wir haben im letzten der Beiträge dieser Kolumne [ JMM10 ] über atomare skalare Variablen gesprochen. In diesem Beitrag sehen wir uns die atomaren Referenzvariablen an. Wir haben im letzten Beitrag gezeigt, 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. Erläutert haben wir es am Beispiel einer Counter-Abstraktion, in der ein Integer konkurrierend inkrementiert und dekrementiert werden kann. Da Inkrement und Dekrement keine atomaren Operationen sind, sind sie unterbrechbar. Eine Möglichkeit, die Counter-Abstraktion thread-safe zu machen, besteht nun darin, den konkurrierenden Zugriff auf diese Operationen mit Hilfe von Locks zu synchronisieren . Als Alternative zur Synchronisation kann man statt eines normalen int einen AtomicInteger verwenden. Er hat nämlich atomare Inkrement- und Dekrement-Operationen, deren Verwendung dann den Verzicht auf Synchronisation ermöglicht.
Ganz allgemein ist die Idee hinter einem atomaren Inkrement und Dekrement
die sogenannte Compare-and-Swap-Sequenz, auch CAS genannt. Solch
ein atomarer CAS-Befehl wird üblicherweise von dem jeweiligen Prozessor
bereits unterstützt und die atomaren Variablen in Java nutzen die
Möglichkeiten der Hardware, um ununterbrechbare Read-Modify-Write-Sequenzen
auf Java-Objekten zu unterstützen. Solche Unterstützung
bieten die atomaren, skalaren Variablen AtomicInteger, AtomicLong und AtomicBoolean.
Nun beschränkt sich das Bedürfnis nach atomaren Read-Modify-Write-Sequenzen
nicht nur auf skalare Typen. Deshalb gibt es neben den atomaren Skalaren
auch noch atomare Referenzen wie AtomicReference<V>, AtomicMarkableReference<V>
und AtomicStampedReference<V>.
Fallstudie zu atomaren ReferenzenDiese atomaren Referenzen wollen wir uns in diesem Beitrag genauer ansehen. Dazu betrachten wir ein Beispiel:public final class StringUpdater { // Vorsicht: falsch !!!Die Klasse StringUpdater enthält einen String und ein Boolesches Flag, welches anzeigt, ob der String seit der letzten Anzeige geändert wurde. Eine solche Abstraktion könnte zum Beispiel verwendet werden, um die Ergebnisse eines Temperatur-Sensors anzuzeigen: ein oder mehrere Sensor-Threads legen in dem StringUpdater mit Hilfe der setText()-Methode einen neuen Wert ab, wenn sich die Temperatur verändert hat, und markieren den String als "neu", indem das Flag gesetzt wird. Ein oder mehrere andere Threads zeigen den Wert über die displayText()-Methode an, aber nur, wenn er "neu" ist. Die Idee ist, dass der Wert nur angezeigt werden soll, wenn er sich geändert hat; wiederholtes Anzeigen desselben Wertes soll vermieden werden. Hingegen ist es kein Problem, wenn mal eine Änderung gar nicht angezeigt wird. Dieses Vorgehen macht Sinn, wenn das Anzeigen teuer oder aufwändig ist. Wenn die "Anzeige" beispielsweise darin besteht, dass jemanden eine Email geschickt wird mit dem neuen Wert, dann möchte man sicher nicht überflüssige Emails schicken, die gar keine neue Information enthalten. Soweit die Randbedingungen. Sehen wir uns nun an, ob die Abstraktion StringUpdater korrekt ist. SichtbarkeitsproblemeDie oben gezeigte Lösung hat gravierende Defizite. Der erste Mangel sind Sichtbarkeitsprobleme. Es ist nicht gewährleistet, dass der Anzeige-Thread überhaupt zu sehen bekommt, was der Sensor-Thread in dem StringUpdater hinterlegt hat.Das Sichtbarkeitsproblem könnte man lösen, indem die beiden Felder der Klasse StringUpdater als volatile deklariert werden. Das sähe dann so aus: public final class StringUpdater { // Vorsicht: immer noch falsch !!!
Das Problem ist, dass die Sequenz von Lesen-Auswerten-Verändern des Booleschen Feldes changed ununterbrechbar sein müsste. Wir brauchen also Synchronisation, um die Mehrfach-Anzeige zu verhindern. Das sähe dann so aus: public final class StringUpdater { // korrekt, aber nicht optimalJetzt ist die Lösung zwar korrekt und thread-sicher, aber wegen der Synchronisation relativ teuer, was die Performance betrifft. Optimierung mit Atomaren ReferenzenWie wir gesehen haben, löst volatile einen Teil des Problems (das Sichtbarkeitsproblem), bietet aber nicht die Ununterbrechbarkeit der Read-Modify-Write-Sequenz, die wir brauchen. Atomare Variablen haben die ununterbrechbare Read-Modify-Write-Operationen, die den volatile-Variablen fehlen, und lösen außerdem die gleichen Speichereffekte aus wie volatile. Versuchen wir eine Lösung mit einer AtomicReference und einem AtomicBoolean.public class StringUpdater { // Vorsicht: falsch !!!Jetzt haben wir eine atomare Read-Modify-Write-Sequenz verwendet, um das Boolesche Feld changed zu bearbeiten. Wenn nun zwei Anzeige-Threads das Boolesche Feld changed lesen und beide den Wert true vorfinden, dann wird einer von beiden Threads erfolgreich das Boolesche Feld modifzieren und anschließend den Text anzeigen. Im jeweils anderen Thread würde das Modifizieren des Booleschen Feld changed scheitern, weil das Boolesche Feld nicht mehr den erwarteten Wert true hat. Das wäre in Ordnung; dann würde der betreffende Thread einfach noch einmal versuchen, das Feld erneut zu lesen, zu ändern und dann den Text anzuzeigen. Leider reicht das aber nicht aus, um die Mehrfach-Anzeigen zu verhindern.
Das Problem hier ist, dass wir das Boolesche Feld changed nur ändern dürfen, wenn sich der Inhalt des Textfeldes nicht geändert hat. Es genügt nicht, wenn wir das Boolesche Feld in Isolation betrachten, sondern wir müssen hier die Kombination von String und Boolean gemeinsam verarbeiten. Für diese Kombination von einem Objekt und einem Booleschen Wert gibt es im Package java.util.concurrent.atomic eine Abstraktion, nämlich die AtomicMarkableReference<V>. Die korrekte Lösung für unser Problem sieht also so aus: public final class StringUpdater { // endlich korrekt und effizient
Wir haben anhand der Fallstudie illustriert, dass die Verwendung von atomaren Referenzen zur Verweidung von Synchronisation verwendet werden kann. Die atomaren Referenzen haben diesselben Speichereffekte wie volatile Referenzen und haben zusätzlich ununterbrechbare CAS-Operationen. In diesem Sinne sind die atomaren Referenzen eine logische Verallgemeinerung von volatile Referenzen. Wie das Beispiel aber auch gezeigt hat, kann man bei Verwendung von atomaren Referenzen natürlich Fehler machen. Die Verwendung von atomaren Variablen funktioniert nur dann als Ersatz für Synchronisation, wenn sich die kritischen Zugriffe auf eine einzige Variable beziehen. Wir haben in der Fallstudie den Fehler gemacht, dass wir zunächst zwei atomare Variablen verwendet haben. Auch das kann in Spezialfällen mal korrekt sein, nämlich wenn die beiden Variablen voneinander unabhängig sind. Sobald jedoch die beiden Variablen in einer Beziehung zueinander stehen und konsistent zueinander sein müssen, dann ist die Verwendung von zwei atomaren Variablen falsch. Wichtig ist beim Programmieren mit atomaren Variablen, dass man sich wirklich auf eine einzige (dann atomare) Variable beschränken kann. In unserem Beispiel war die AtomicMarkableReference<V>, also die Kombination von Referenz und Boolschem Wert, das richtige Instrument. Atomare Referenzen - ÜbersichtWerfen wir zu Schluss noch einen Blick auf die übrigen atomaren Referenz-Abstraktionen. Das Package java.util.concurrent.atomic hat, wie schon erwähnt, gleich 3 Arten von atomaren Referenzen zu bieten: AtomicReference<V>, AtomicMarkableReference<V> und AtomicStampedReference<V>. Die AtomicReference<V> ist die "normale" atomare Referenz auf ein Objekt. Die AtomicMarkableReference<V> ist eine Referenz auf ein Objekt zusammen mit einem Booleschen Wert; dabei ist der Boolesche Wert typischerweise sowas wie eine Gültigkeitsinformation zu dem Objekt. Die AtomicStampedReference<V> ist eine Referenz zusammen mit einem Integer-Wert; dabei ist der Integer häufig eine Art Versionsnummer für das referenzierte Objekt.
Alle atomaren Referenzen haben dieselben Speichereffekte wir volatile,
damit es keine Sichtbarkeitsprobleme gibt. Die wesentlichen Methoden
und deren Speichereffekte sind:
Neben den oben genannten Abstraktionen gibt es noch die schon im letzten Beitrag erwähnten atomaren Skalare AtomicBoolean, AtomicInteger, und AtomicLong. Sie haben zusätzliche Inkrement- und Dekrement-Methoden. Außerdem gibt es atomare Arrays: AtomicIntegerArray, AtomicLongArray und AtomicReferenceArray. Sie bieten atomare Zugriffe auf die Elemente des Arrays - eine Funktionalität, die es für normale Arrays nicht gibt, weil man mit den Java-Sprachmitteln gar nicht deklarieren kann, dass man ein Array von volatile-Elementen haben möchte. Daneben gibt es noch die sogenannten atomaren Updater: AtomicReferenceFieldUpdater<T,V>, AtomicIntegerFieldUpdater<T> und AtomicLongFieldUpdater<T>. Das sind Abstraktionen, die atomare CAS-Zugriffe auf volatile-Felder von Objekten anbieten. Während die atomaren Variablen jeweils ein Objekt (oder skalaren Wert, Array, Objekt + Boolean, Objekt + Version) kapseln und für das Objekt atomare CAS-Zugriffe ermöglichen, bieten die Updater atomare CAS-Zugriffe für volatile-Felder von Objekten. Dabei kann man einen einzigen Updater verwenden, um auf ein bestimmtes Feld in verschiedenen Objekten zuzugreifen. Man hat also zum Beispiel nur einen Updater für das next-Feld einer Klasse Node und kann mit diesem einen Updater auf das next-Feld aller Objekte vom Typ Node zugreifen. Das spart Resourcen, weil nicht jedes einzelne next-Feld in jedem einzelen Node-Objekt in eine AtomicReference<Node> eingepackt werden muss. Dafür sind die Garantien schwächer, weil neben den sicheren Zugriffen über den Updater immer noch direkt Zugriffe auf die volatile-Fehler gemacht werden können, die dann nicht atomar und ohne Speichereffekte sind. Praxis-RelevanzAtomare Variablen und Updater sind neben volatile eine weitere Möglichkeit, konkurrierende Zugriffe auf gemeinsam verwendete Objekte zu optimieren, indem man anstelle von synchronisierten Zugriffen atomare Zugriffe macht. Dieses Vermeiden von Synchronisation wird oft als Lockfree-Programming bezeichnet. Lockfree-Programming ist aber keine Technik für den Hausgebrauch, denn der Verzicht auf Synchronisation ist nur speziellen Fällen möglich. Voraussetzung für die Nutzung von atomaren Variablen (und den Verzicht auf Synchronisation) ist, dass die wesentlichen Zugriffe sich auf eine einzige Variable beschränken lassen, auf die man dann atomar zugreifen kann. Sobald mehrere Variablen konsistent zueinander geändert werden müssen, reichen atomare Variablen nicht aus; Synchronisation ist dann zwingend erforderlich.Beispiele für den Einsatz atomarer Variablen findet man im JDK: die Klasse ConcurrentLinkedQueue wie auch die SkipList-Implementierungen sind Beispiele für Abstraktionen, die thread-sicher sind und die Thread-Sicherheit ohne die Verwendung von Synchronisation erreicht. Die Implementierung der ConcurrentLinkedQueue zum Beispiel verwendet atomare Updater, um den Link auf den jeweils nächsten Knoten in der Liste zu verwalten. Die Algorithmen, die für diese Art von Programmierung benötigt werden, sind in der Regeln nicht trivial und typischerweise das Ergebnis jahrelanger Forschung. Es gibt einige Standardalgorithmen für Stacks und Listen. Wer sich für Details interessiert, der muss sich mit der entsprechenden Fachliteratur beschäftigen. Eine gute (wenn auch sehr, sehr knappe) Einführung in die Prinzipien des Lockfree-Programming findet man in Kapitel 15.4. "Nonblocking Algorithms" in "Java Concurrency in Practice" von Brian Goetz (siehe [ JCP ]). Insgesamt ist die Verwendung von atomaren Variablen kein Instrument, mit dem man in beliebigen Situationen optimieren kann, indem synchronisierte Zugriffe durch atomare Zugriffe ersetzt werden. ZusammenfassungIn diesem Beitrag haben wir uns die atomaren Referenzvariablen angesehen. Sie sind eine logische Verallgemeinerung von volatile Referenzen. Die atomaren Referenzen haben diesselben Speichereffekte wie volatile Referenzen und stellen zusätzlich ununterbrechbare CAS-Operationen zur Verfügung. Atomare Referenzen ermöglichen Optimierungen, die darin bestehen, dass auf Synchronisation mit Locks verzichtet wird und stattdessen lockfree programmiert wird. Wichtig ist beim Programmieren mit atomaren Variablen, dass man die kritischen Zugriffe auf eine einzige Variable beschränken kann.
Literaturverweise
Die gesamte Serie über das Java Memory Model:
|
|||||||||||||||||||||||||||||||||||||||||||||||||
© Copyright 1995-2015 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/47.JMM-AtomicReference/47.JMM-AtomicReference.html> last update: 22 Mar 2015 |