JavaRush /Java-Blog /Random-DE /Volatilität managen
lexmirnov
Level 29
Москва

Volatilität managen

Veröffentlicht in der Gruppe Random-DE

Richtlinien für die Verwendung flüchtiger Variablen

Von Brian Goetz, 19. Juni 2007 Original: Verwalten der Volatilität Flüchtige Variablen in Java können als „synchronized-light“ bezeichnet werden. Sie benötigen weniger Code als synchronisierte Blöcke, laufen oft schneller, können aber nur einen Bruchteil dessen leisten, was synchronisierte Blöcke leisten. In diesem Artikel werden verschiedene Muster für den effektiven Einsatz von Volatilität vorgestellt – und einige Warnungen, wo man es nicht verwenden sollte. Sperren haben zwei Hauptmerkmale: gegenseitigen Ausschluss (Mutex) und Sichtbarkeit. Gegenseitiger Ausschluss bedeutet, dass eine Sperre jeweils nur von einem Thread gehalten werden kann. Mit dieser Eigenschaft können Zugriffskontrollprotokolle für gemeinsam genutzte Ressourcen implementiert werden, sodass jeweils nur ein Thread diese verwenden kann. Sichtbarkeit ist ein subtileres Problem. Ihr Zweck besteht darin, sicherzustellen, dass Änderungen, die an öffentlichen Ressourcen vorgenommen werden, bevor die Sperre aufgehoben wird, für den nächsten Thread sichtbar sind, der diese Sperre übernimmt. Wenn die Synchronisierung keine Sichtbarkeit gewährleisten würde, könnten Threads veraltete oder falsche Werte für öffentliche Variablen erhalten, was zu einer Reihe schwerwiegender Probleme führen würde.
Flüchtige Variablen
Flüchtige Variablen haben die Sichtbarkeitseigenschaften synchronisierter Variablen, ihnen fehlt jedoch deren Atomizität. Dies bedeutet, dass Threads automatisch die aktuellsten Werte flüchtiger Variablen verwenden. Sie können zur Thread-Sicherheit verwendet werden , jedoch in einer sehr begrenzten Anzahl von Fällen: solche, die keine Beziehungen zwischen mehreren Variablen oder zwischen aktuellen und zukünftigen Werten einer Variablen einführen. Somit reicht volatile allein nicht aus, um einen Zähler, einen Mutex oder eine Klasse zu implementieren, deren unveränderliche Teile mit mehreren Variablen verknüpft sind (z. B. „start <=end“). Sie können flüchtige Sperren aus einem von zwei Hauptgründen wählen: Einfachheit oder Skalierbarkeit. Einige Sprachkonstrukte lassen sich leichter als Programmcode schreiben und später lesen und verstehen, wenn sie flüchtige Variablen anstelle von Sperren verwenden. Außerdem können sie im Gegensatz zu Sperren keinen Thread blockieren und sind daher weniger anfällig für Skalierbarkeitsprobleme. In Situationen, in denen es viel mehr Lese- als Schreibvorgänge gibt, können flüchtige Variablen Leistungsvorteile gegenüber Sperren bieten.
Bedingungen für die korrekte Verwendung von Volatilität
Unter bestimmten Umständen können Sie Schlösser durch flüchtige Schlösser ersetzen. Um Thread-sicher zu sein, müssen beide Kriterien erfüllt sein:
  1. Was in eine Variable geschrieben wird, ist unabhängig von ihrem aktuellen Wert.
  2. Die Variable nimmt nicht an Invarianten mit anderen Variablen teil.
Einfach ausgedrückt bedeuten diese Bedingungen, dass die gültigen Werte, die in eine flüchtige Variable geschrieben werden können, unabhängig von jedem anderen Status des Programms sind, einschließlich des aktuellen Status der Variablen. Die erste Bedingung schließt die Verwendung flüchtiger Variablen als Thread-sichere Zähler aus. Obwohl Inkrementieren (x++) wie eine einzelne Operation aussieht, handelt es sich tatsächlich um eine ganze Folge von Lese-, Änderungs- und Schreiboperationen, die atomar ausgeführt werden müssen, was volatile nicht bietet. Eine gültige Operation würde erfordern, dass der Wert von x während der gesamten Operation gleich bleibt, was mit volatile nicht erreicht werden kann. (Wenn Sie jedoch sicherstellen können, dass der Wert nur von einem Thread geschrieben wird, kann die erste Bedingung weggelassen werden.) In den meisten Situationen wird entweder die erste oder die zweite Bedingung verletzt, wodurch flüchtige Variablen ein weniger häufig verwendeter Ansatz zur Erreichung der Thread-Sicherheit sind als synchronisierte Variablen. Listing 1 zeigt eine nicht threadsichere Klasse mit einem Zahlenbereich. Es enthält eine Invariante – die Untergrenze ist immer kleiner oder gleich der Obergrenze. @NotThreadSafe public class NumberRange { private int lower, upper; public int getLower() { return lower; } public int getUpper() { return upper; } public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...); upper = value; } } Da Bereichszustandsvariablen auf diese Weise begrenzt sind, reicht es nicht aus, die unteren und oberen Felder flüchtig zu machen, um sicherzustellen, dass die Klasse Thread-sicher ist. Eine Synchronisierung ist weiterhin erforderlich. Andernfalls haben Sie früher oder später Pech und zwei Threads, die setLower() und setUpper() mit ungeeigneten Werten ausführen, können dazu führen, dass der Bereich inkonsistent wird. Wenn der Anfangswert beispielsweise (0, 5) ist, Thread A setLower(4) aufruft und gleichzeitig Thread B setUpper(3) aufruft, führen diese verschachtelten Vorgänge zu einem Fehler, obwohl beide die Prüfung bestehen das soll die Invariante schützen. Infolgedessen beträgt der Bereich (4, 3) – falsche Werte. Wir müssen setLower() und setUpper() atomar für andere Bereichsoperationen machen – und wenn wir Felder flüchtig machen, reicht das nicht aus.
Leistungsüberlegungen
Der erste Grund für die Verwendung von volatile ist die Einfachheit. In manchen Situationen ist die Verwendung einer solchen Variablen einfach einfacher als die Verwendung der damit verbundenen Sperre. Der zweite Grund ist die Leistung. Manchmal funktioniert Volatilität schneller als Sperren. Es ist äußerst schwierig, präzise und umfassende Aussagen wie „X ist immer schneller als Y“ zu treffen, insbesondere wenn es um die internen Abläufe der Java Virtual Machine geht. (Zum Beispiel kann die JVM in manchen Situationen die Sperre vollständig aufheben, was es schwierig macht, die Kosten von Volatilität gegenüber Synchronisierung abstrakt zu diskutieren.) Bei den meisten modernen Prozessorarchitekturen unterscheiden sich die Kosten für das Lesen flüchtiger Variablen jedoch nicht wesentlich von den Kosten für das Lesen regulärer Variablen. Aufgrund der für die Sichtbarkeit erforderlichen Speichereingrenzung sind die Kosten für das Schreiben flüchtiger Variablen deutlich höher als für das Schreiben regulärer Variablen, im Allgemeinen jedoch günstiger als das Setzen von Sperren.
Muster für die ordnungsgemäße Verwendung von Volatilität
Viele Parallelitätsexperten neigen dazu, die Verwendung flüchtiger Variablen gänzlich zu vermeiden, da ihre korrekte Verwendung schwieriger ist als Sperren. Es gibt jedoch einige klar definierte Muster, die bei sorgfältiger Befolgung in einer Vielzahl von Situationen sicher angewendet werden können. Beachten Sie immer die Einschränkungen von volatile – verwenden Sie nur volatile, die unabhängig von allem anderen im Programm sind, und das sollte verhindern, dass Sie mit diesen Mustern in gefährliches Terrain geraten.
Muster Nr. 1: Statusflags
Möglicherweise handelt es sich bei der kanonischen Verwendung veränderlicher Variablen um einfache boolesche Statusflags, die anzeigen, dass ein wichtiges einmaliges Lebenszyklusereignis aufgetreten ist, beispielsweise der Abschluss der Initialisierung oder eine Anforderung zum Herunterfahren. Viele Anwendungen enthalten ein Kontrollkonstrukt der Form: „Bis wir zum Herunterfahren bereit sind, laufen Sie weiter“, wie in Listing 2 gezeigt: Es ist volatile boolean shutdownRequested; ... public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } } wahrscheinlich, dass die Methode „shutdown()“ von irgendwo außerhalb der Schleife – in einem anderen Thread – aufgerufen wird. Daher ist eine Synchronisierung erforderlich, um die korrekte Sichtbarkeit der Variablen „shutdownRequested“ sicherzustellen. (Es kann von einem JMX-Listener, einem Aktions-Listener in einem GUI-Ereignisthread, über RMI, über einen Webdienst usw. aufgerufen werden.) Allerdings ist eine Schleife mit synchronisierten Blöcken viel umständlicher als eine Schleife mit einem flüchtigen Statusflag wie in Listing 2. Da volatile das Schreiben von Code erleichtert und das Statusflag von keinem anderen Programmstatus abhängt, ist dies ein Beispiel für a gute Verwendung von Volatilität. Das Charakteristische an solchen Statusflags ist, dass es in der Regel nur einen Zustandsübergang gibt; Das Flag „shutdownRequested“ wechselt von „false“ auf „true“ und das Programm wird dann heruntergefahren. Dieses Muster kann auf Zustandsflags erweitert werden, die sich hin und her ändern können, jedoch nur, wenn der Übergangszyklus (von falsch über wahr nach falsch) ohne externes Eingreifen erfolgt. Andernfalls ist eine Art atomarer Übergangsmechanismus, beispielsweise atomare Variablen, erforderlich.
Muster Nr. 2: Einmalige sichere Veröffentlichung
Sichtbarkeitsfehler, die bei fehlender Synchronisierung auftreten können, können beim Schreiben von Objektreferenzen anstelle von Grundwerten zu einem noch schwierigeren Problem werden. Ohne Synchronisierung können Sie den aktuellen Wert für eine Objektreferenz sehen, die von einem anderen Thread geschrieben wurde, und dennoch veraltete Statuswerte für dieses Objekt sehen. (Diese Bedrohung ist die Ursache des Problems mit der berüchtigten Double-Check-Sperre, bei der eine Objektreferenz ohne Synchronisierung gelesen wird und Sie riskieren, die tatsächliche Referenz zu sehen, aber dadurch ein teilweise erstelltes Objekt zu erhalten.) Eine Möglichkeit, eine sicher zu veröffentlichen Objekt dient dazu, auf ein flüchtiges Objekt zu verweisen. Listing 3 zeigt ein Beispiel, bei dem während des Startvorgangs ein Hintergrundthread einige Daten aus der Datenbank lädt. Anderer Code, der möglicherweise versucht, diese Daten zu verwenden, prüft, ob sie veröffentlicht wurden, bevor er versucht, sie zu verwenden. public class BackgroundFloobleLoader { public volatile Flooble theFlooble; public void initInBackground() { // делаем много всякого theFlooble = new Flooble(); // единственная запись в theFlooble } } public class SomeOtherClass { public void doWork() { while (true) { // чё-то там делаем... // используем theFolooble, но только если она готова if (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble); } } } Wenn der Verweis auf theFlooble nicht flüchtig wäre, würde der Code in doWork() riskieren, einen teilweise konstruierten Flooble zu sehen, wenn er versucht, auf theFlooble zu verweisen. Die Hauptanforderung für dieses Muster besteht darin, dass das veröffentlichte Objekt entweder threadsicher oder effektiv unveränderlich sein muss (effektiv unveränderlich bedeutet, dass sich sein Status nach der Veröffentlichung nie ändert). Ein flüchtiger Link kann sicherstellen, dass ein Objekt in seiner veröffentlichten Form sichtbar ist. Wenn sich jedoch der Status des Objekts nach der Veröffentlichung ändert, ist eine zusätzliche Synchronisierung erforderlich.
Muster Nr. 3: Unabhängige Beobachtungen
Ein weiteres einfaches Beispiel für eine sichere Verwendung von Volatilität ist die regelmäßige „Veröffentlichung“ von Beobachtungen zur Verwendung innerhalb eines Programms. Beispielsweise gibt es einen Umgebungssensor, der die aktuelle Temperatur erfasst. Der Hintergrundthread kann diesen Sensor alle paar Sekunden auslesen und eine flüchtige Variable aktualisieren, die die aktuelle Temperatur enthält. Andere Threads können diese Variable dann lesen und wissen, dass der darin enthaltene Wert immer aktuell ist. Eine weitere Verwendung dieses Musters ist das Sammeln von Statistiken über das Programm. Listing 4 zeigt, wie sich der Authentifizierungsmechanismus den Namen des zuletzt angemeldeten Benutzers merken kann. Die lastUser-Referenz wird wiederverwendet, um den Wert zur Verwendung durch den Rest des Programms zu veröffentlichen. public class UserManager { public volatile String lastUser; public boolean authenticate(String user, String password) { boolean valid = passwordIsValid(user, password); if (valid) { User u = new User(); activeUsers.add(u); lastUser = user; } return valid; } } Dieses Muster erweitert das vorherige; Der Wert wird zur Verwendung an anderer Stelle im Programm veröffentlicht, die Veröffentlichung ist jedoch kein einmaliges Ereignis, sondern eine Reihe unabhängiger Ereignisse. Dieses Muster erfordert, dass der veröffentlichte Wert praktisch unveränderlich ist – dass sich sein Zustand nach der Veröffentlichung nicht ändert. Code, der den Wert verwendet, muss sich darüber im Klaren sein, dass er sich jederzeit ändern kann.
Muster Nr. 4: Muster „flüchtige Bohne“.
Das „Volatile Bean“-Muster ist in Frameworks anwendbar, die JavaBeans als „verherrlichte Strukturen“ verwenden. Das „Volatile Bean“-Muster verwendet eine JavaBean als Container für eine Gruppe unabhängiger Eigenschaften mit Gettern und/oder Settern. Der Grund für das „Volatile Bean“-Muster liegt darin, dass viele Frameworks Container für veränderbare Datenbehälter (z. B. HttpSession) bereitstellen, die in diesen Containern platzierten Objekte jedoch Thread-sicher sein müssen. Im flüchtigen Bean-Muster sind alle JavaBean-Datenelemente flüchtig, und Getter und Setter sollten trivial sein – sie sollten keine andere Logik als das Abrufen oder Festlegen der entsprechenden Eigenschaft enthalten. Darüber hinaus müssen Datenelemente, die Objektreferenzen sind, tatsächlich unveränderlich sein. (Dadurch sind Array-Referenzfelder nicht zulässig, da, wenn eine Array-Referenz als flüchtig deklariert wird, nur diese Referenz und nicht die Elemente selbst über die volatile-Eigenschaft verfügt.) Wie bei jeder flüchtigen Variablen können den Eigenschaften von JavaBeans keine Invarianten oder Einschränkungen zugeordnet sein . Ein Beispiel für eine JavaBean, die mit dem „Volatile Bean“-Muster geschrieben wurde, ist in Listing 5 dargestellt: @ThreadSafe public class Person { private volatile String firstName; private volatile String lastName; private volatile int age; public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; } public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) { this.lastName = lastName; } public void setAge(int age) { this.age = age; } }
Komplexere volatile Muster
Die Muster im vorherigen Abschnitt decken die meisten häufigen Fälle ab, in denen die Verwendung von volatile sinnvoll und offensichtlich ist. In diesem Abschnitt wird ein komplexeres Muster betrachtet, bei dem Volatilität einen Leistungs- oder Skalierbarkeitsvorteil bieten kann. Fortgeschrittenere volatile Muster können äußerst fragil sein. Es ist wichtig, dass Ihre Annahmen sorgfältig dokumentiert werden und dass diese Muster stark gekapselt sind, denn selbst die kleinsten Änderungen können Ihren Code beschädigen! Da der Hauptgrund für komplexere, volatile Anwendungsfälle die Leistung ist, stellen Sie außerdem sicher, dass Sie tatsächlich einen klaren Bedarf für die beabsichtigte Leistungssteigerung haben, bevor Sie sie verwenden. Bei diesen Mustern handelt es sich um Kompromisse, die die Lesbarkeit oder Wartbarkeit für mögliche Leistungssteigerungen beeinträchtigen. Wenn Sie die Leistungsverbesserung nicht benötigen (oder nicht mit einem strengen Messprogramm nachweisen können, dass Sie sie benötigen), ist das wahrscheinlich ein schlechtes Geschäft Sie geben etwas Wertvolles auf und erhalten dafür etwas weniger.
Muster Nr. 5: Günstige Lese-/Schreibsperre
Mittlerweile sollten Sie sich darüber im Klaren sein, dass Volatilität zu schwach ist, um einen Zähler zu implementieren. Da es sich bei ++x im Wesentlichen um eine Reduzierung von drei Vorgängen (Lesen, Anhängen, Speichern) handelt, verlieren Sie im Falle eines Fehlers den aktualisierten Wert, wenn mehrere Threads gleichzeitig versuchen, den flüchtigen Zähler zu erhöhen. Wenn es jedoch deutlich mehr Lesevorgänge als Änderungen gibt, können Sie intrinsische Sperren und flüchtige Variablen kombinieren, um den Gesamtaufwand für den Codepfad zu reduzieren. Listing 6 zeigt einen Thread-sicheren Zähler, der synchronisiert verwendet, um sicherzustellen, dass die Inkrementierungsoperation atomar ist, und volatile verwendet, um sicherzustellen, dass das aktuelle Ergebnis sichtbar ist. Wenn Aktualisierungen selten erfolgen, kann dieser Ansatz die Leistung verbessern, da die Lesekosten auf flüchtige Lesevorgänge beschränkt sind, die im Allgemeinen günstiger sind als der Erwerb einer nicht widersprüchlichen Sperre. @ThreadSafe public class CheesyCounter { // Employs the cheap read-write lock trick // All mutative operations MUST be done with the 'this' lock held @GuardedBy("this") private volatile int value; public int getValue() { return value; } public synchronized int increment() { return value++; } } Der Grund dafür, dass diese Methode als „billige Lese-/Schreibsperre“ bezeichnet wird, liegt darin, dass Sie unterschiedliche Timing-Mechanismen für Lese- und Schreibvorgänge verwenden. Da Schreibvorgänge in diesem Fall gegen die erste Bedingung der Verwendung von volatile verstoßen, können Sie volatile nicht zur sicheren Implementierung eines Zählers verwenden – Sie müssen eine Sperre verwenden. Sie können jedoch volatile verwenden, um den aktuellen Wert beim Lesen sichtbar zu machen. Sie verwenden also eine Sperre für alle Änderungsvorgänge und volatile für schreibgeschützte Vorgänge. Wenn eine Sperre jeweils nur einem Thread den Zugriff auf einen Wert erlaubt, erlauben flüchtige Lesevorgänge mehr als einen. Wenn Sie also flüchtig zum Schutz des Lesevorgangs verwenden, erhalten Sie ein höheres Maß an Austausch, als wenn Sie eine Sperre für den gesamten Code verwenden: und liest. und zeichnet auf. Beachten Sie jedoch die Fragilität dieses Musters: Bei zwei konkurrierenden Synchronisationsmechanismen kann es sehr komplex werden, wenn Sie über die grundlegendste Anwendung dieses Musters hinausgehen.
Zusammenfassung
Flüchtige Variablen sind eine einfachere, aber schwächere Form der Synchronisierung als Sperren, die in manchen Fällen eine bessere Leistung oder Skalierbarkeit bietet als intrinsische Sperren. Wenn Sie die Bedingungen für die sichere Verwendung von volatile erfüllen – eine Variable ist wirklich unabhängig sowohl von anderen Variablen als auch von ihren eigenen vorherigen Werten – können Sie manchmal den Code vereinfachen, indem Sie synchronisiert durch volatile ersetzen. Allerdings ist Code, der volatile verwendet, oft anfälliger als Code, der Sperren verwendet. Die hier vorgeschlagenen Muster decken die häufigsten Fälle ab, in denen Volatilität eine sinnvolle Alternative zur Synchronisierung darstellt. Indem Sie diesen Mustern folgen – und darauf achten, sie nicht über ihre eigenen Grenzen hinauszutreiben – können Sie volatile dort sicher einsetzen, wo sie Vorteile bieten.
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION