JavaRush /Java Blog /Random EN /Managing volatility
lexmirnov
Level 29
Москва

Managing volatility

Published in the Random EN group

Guidelines for Using Volatile Variables

By Brian Goetz June 19, 2007 Original: Managing Volatility Volatile variables in Java can be called "synchronized-light"; They require less code to use than synchronized blocks, often run faster, but can only do a fraction of what synchronized blocks do. This article presents several patterns for using volatile effectively—and a few warnings about where not to use it. Locks have two main features: mutual exclusion (mutex) and visibility. Mutual exclusion means that a lock can only be held by one thread at a time, and this property can be used to implement access control protocols for shared resources so that only one thread will use them at a time. Visibility is a more subtle issue, its purpose is to ensure that changes made to public resources before the lock is released will be visible to the next thread that takes over that lock. If synchronization did not guarantee visibility, threads could receive stale or incorrect values ​​for public variables, which would lead to a number of serious problems.
Volatile variables
Volatile variables have the visibility properties of synchronized ones, but lack their atomicity. This means that threads will automatically use the most current values ​​of volatile variables. They can be used for thread safety , but in a very limited set of cases: those that do not introduce relationships between multiple variables or between current and future values ​​of a variable. Thus, volatile alone is not enough to implement a counter, a mutex, or any class whose immutable parts are associated with multiple variables (for example, "start <=end"). You can choose volatile locks for one of two main reasons: simplicity or scalability. Some language constructs are easier to write as program code, and later to read and understand, when they use volatile variables instead of locks. Also, unlike locks, they cannot block a thread and are therefore less prone to scalability issues. In situations where there are many more reads than writes, volatile variables can provide performance benefits over locks.
Conditions for correct use of volatile
You can replace locks with volatile ones in a limited number of circumstances. To be thread safe, both criteria must be met:
  1. What is written to a variable is independent of its current value.
  2. The variable does not participate in invariants with other variables.
Simply put, these conditions mean that the valid values ​​that can be written to a volatile variable are independent of any other state of the program, including the current state of the variable. The first condition excludes the use of volatile variables as thread-safe counters. Although increment (x++) looks like a single operation, it is actually a whole sequence of read-modify-write operations that must be performed atomically, which volatile does not provide. A valid operation would require that the value of x remain the same throughout the operation, which cannot be achieved using volatile. (However, if you can ensure that the value is written from only one thread, the first condition can be omitted.) In most situations, either the first or second conditions will be violated, making volatile variables a less commonly used approach to achieving thread safety than synchronized ones. Listing 1 shows a non-thread-safe class with a range of numbers. It contains an invariant - the lower bound is always less than or equal to the upper. @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; } } Since range state variables are limited in this way, it will not be enough to make the lower and upper fields volatile to ensure the class is thread safe; synchronization will still be needed. Otherwise, sooner or later you will be unlucky and two threads performing setLower() and setUpper() with inappropriate values ​​can lead the range to an inconsistent state. For example, if the initial value is (0, 5), thread A calls setLower(4), and at the same time thread B calls setUpper(3), these interleaved operations will result in an error, although both will pass the check that is supposed to protect the invariant. As a result, the range will be (4, 3) - incorrect values. We need to make setLower() and setUpper() atomic to other range operations - and making fields volatile won't do that.
Performance Considerations
The first reason to use volatile is simplicity. In some situations, using such a variable is simply easier than using the lock associated with it. The second reason is performance, sometimes volatile will work faster than locks. It is extremely difficult to make precise, all-encompassing statements like "X is always faster than Y," especially when it comes to the internal operations of the Java Virtual Machine. (For example, the JVM may release the lock entirely in some situations, making it difficult to discuss the costs of volatile versus synchronization in an abstract way). However, on most modern processor architectures, the cost of reading volatile is not much different from the cost of reading regular variables. The cost of writing volatile is significantly higher than writing regular variables due to the memory fencing required for visibility, but is generally cheaper than setting locks.
Patterns for proper use of volatile
Many concurrency experts tend to avoid using volatile variables altogether because they are more difficult to use correctly than locks. However, there are some well-defined patterns that, if followed carefully, can be used safely in a wide variety of situations. Always respect the limitations of volatile - only use volatiles that are independent of anything else in the program, and this should keep you from getting into dangerous territory with these patterns.
Pattern #1: Status Flags
Perhaps the canonical use of mutable variables is simple boolean status flags indicating that an important one-time lifecycle event has occurred, such as initialization completion or a shutdown request. Many applications include a control construct of the form: "until we are ready to shut down, keep running" as shown in Listing 2: It is volatile boolean shutdownRequested; ... public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } } likely that the shutdown() method will be called from somewhere outside the loop - on another thread - so synchronization is required to ensure correct variable visibility shutdownRequested. (It can be called from a JMX listener, an action listener in a GUI event thread, via RMI, via a web service, etc.). However, a loop with synchronized blocks will be much more cumbersome than a loop with a volatile state flag as in Listing 2. Because volatile makes writing code easier and the state flag does not depend on any other program state, this is an example of a good use of volatile. The characteristic of such status flags is that there is usually only one state transition; the shutdownRequested flag goes from false to true, and then the program shuts down. This pattern can be extended to state flags that can change back and forth, but only if the transition cycle (from false to true to false) occurs without external intervention. Otherwise some kind of atomic transition mechanism, such as atomic variables, is needed.
Pattern #2: One-time secure publishing
Visibility errors that are possible when there is no synchronization can become an even more difficult issue when writing object references instead of primitive values. Without synchronization, you can see the current value for an object reference that was written by another thread and still see stale state values ​​for that object. (This threat is at the root of the problem with the infamous double-check lock, where an object reference is read without synchronization, and you risk seeing the actual reference but getting a partially constructed object through it.) One way to securely publish an object is to make a reference to a volatile object. Listing 3 shows an example where, during startup, a background thread loads some data from the database. Other code that might try to use this data checks to see if it has been published before trying to use it. 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); } } } If the reference to theFlooble were not volatile, the code in doWork() would risk seeing a partially constructed Flooble when attempting to reference theFlooble. The key requirement for this pattern is that the published object must be either thread-safe or effectively immutable (effectively immutable means that its state never changes after it is published). A Volatile link can ensure that an object is visible in its published form, but if the object's state changes after publishing, additional synchronization is required.
Pattern #3: Independent Observations
Another simple example of a safe use of volatile is when observations are periodically “published” for use within a program. For example, there is an environmental sensor that detects the current temperature. The background thread can read this sensor every few seconds and update a volatile variable containing the current temperature. Other threads can then read this variable, knowing that the value in it is always up to date. Another use for this pattern is collecting statistics about the program. Listing 4 shows how the authentication mechanism can remember the name of the last logged in user. The lastUser reference will be reused to post the value for use by the rest of the program. 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; } } This pattern expands on the previous one; the value is published for use elsewhere in the program, but the publication is not a one-time event, but a series of independent ones. This pattern requires that the published value be effectively immutable - that its state does not change after publishing. Code using the value must be aware that it can change at any time.
Pattern #4: “volatile bean” pattern
The “volatile bean” pattern is applicable in frameworks that use JavaBeans as “glorified structs”. The “volatile bean” pattern uses a JavaBean as a container for a group of independent properties with getters and/or setters. The rationale for the "volatile bean" pattern is that many frameworks provide containers for mutable data holders (such as HttpSession), but the objects placed in these containers must be thread-safe. In the volatile bean pattern, all JavaBean data elements are volatile, and getters and setters should be trivial - they should not contain any logic other than getting or setting the corresponding property. Additionally, for data members that are object references, said objects must be effectively immutable. (This disallows array reference fields, since when an array reference is declared volatile, only that reference, and not the elements themselves, has the volatile property.) As with any volatile variable, there can be no invariants or restrictions associated with properties of JavaBeans. An example of a JavaBean written using the “volatile bean” pattern is shown in Listing 5: @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; } }
More complex volatile patterns
The patterns in the previous section cover most of the common cases where using volatile is reasonable and obvious. This section looks at a more complex pattern in which volatile can provide a performance or scalability benefit. More advanced volatile patterns can be extremely fragile. It's critical that your assumptions are carefully documented and that these patterns are strongly encapsulated, because even the smallest changes can break your code! Also, given that the primary reason for more complex volatile use cases is performance, make sure you actually have a clear need for the intended performance gain before using them. These patterns are compromises that sacrifice readability or ease of maintainability for possible performance gains - if you don't need the performance improvement (or can't prove that you need it with a rigorous measurement program), then it's probably a bad deal because that you are giving up something valuable and getting something less in return.
Pattern #5: Cheap read-write lock
By now you should be well aware that volatile is too weak to implement a counter. Since ++x is essentially a reduction of three operations (read, append, store), if things go wrong, you will lose the updated value if multiple threads try to increment the volatile counter at the same time. However, if there are significantly more reads than changes, you can combine intrinsic locking and volatile variables to reduce overall code path overhead. Listing 6 shows a thread-safe counter that uses synchronized to ensure that the increment operation is atomic, and uses volatile to ensure that the current result is visible. If updates are infrequent, this approach can improve performance since read costs are limited to volatile reads, which are generally cheaper than acquiring a non-conflicting lock. @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++; } } The reason this method is called "cheap read-write lock" is because you use different timing mechanisms for reads and writes. Because write operations in this case violate the first condition of using volatile, you cannot use volatile to implement a counter safely - you must use a lock. However, you can use volatile to make the current value visible when reading, so you use a lock for all modification operations and volatile for read-only operations. If a lock only allows one thread at a time to access a value, volatile reads allow more than one, so when you use volatile to protect the read, you get a higher level of exchange than if you use a lock on all code: and reads. and records. However, be aware of the fragility of this pattern: with two competing synchronization mechanisms, it can become very complex if you go beyond the most basic application of this pattern.
Summary
Volatile variables are a simpler but weaker form of synchronization than locking, which in some cases provides better performance or scalability than intrinsic locking. If you meet the conditions for safe use of volatile - a variable is truly independent of both other variables and its own previous values ​​- you can sometimes simplify the code by replacing synchronized with volatile. However, code that uses volatile is often more fragile than code that uses locking. The patterns suggested here cover the most common cases where volatility is a reasonable alternative to synchronization. By following these patterns - and taking care not to push them beyond their own limits - you can safely use volatile in cases where they provide benefits.
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION