Level: Intermediate Brian Goetz, Senior Engineer, Sun Microsystems July 05, 2007
The Java language includes two internal synchronization mechanisms: Synchronous block (or method) and volatile variables. Both mechanisms are proposed to achieve code thread security. Among them, the synchronization of volatile variables is poor (but sometimes it is simpler and has lower overhead), and its usage is more error-prone. In this issueJava Theory and PracticeBrian Goetz will introduce several modes for correctly using volatile variables, and give some suggestions on their applicability restrictions.
The volatile variable in Java can be seen as a "to a lesser extent"synchronized "; Andsynchronized Compared with the block, the volatile variable requires less encoding and less runtime overhead. However, the only function it can implement issynchronized . This article introduces several effective modes for Using volatile variables, and emphasizes the situations where volatile variables are not suitable for use. The lock provides two main features:Mutual Exclusion)AndVisibility). Mutex allows only one thread to hold a specific lock at a time. Therefore, you can use this feature to implement a coordinated access protocol for shared data. In this way, only one thread can use the shared data at a time. Visibility is more complex. It must ensure that changes made to the shared data before the lock is released are visible to another thread that subsequently acquires the lock-if this visibility guarantee is not provided by the synchronization mechanism, the shared variables seen by the thread may be values before modification or inconsistent values, which will cause many serious problems. Volatile variable The volatile variable hassynchronized But not atomic. This means that the thread can automatically discover the latest value of the volatile variable. Volatile variables can be used to provide thread security, but can only be used in a very limited set of Use Cases: There is no constraint between multiple variables or between the current value of a variable and the modified value of a variable. Therefore, using volatile alone is not enough to implement counters, mutex locks, or any class that has an invariants related to multiple variables (such as "Start <= end "). For simplicity or scalability, you may prefer to use volatile variables instead of locks. Some usage (idiom) is easier to code and read when volatile variables are used instead of locks. In addition, the volatile variable does not cause thread blocking like a lock, so it rarely causes scalability problems. In some cases, if the read operation is far greater than the write operation, the volatile variable can also provide performance advantages over the lock. Conditions for correct use of volatile Variables You can only replace the lock with the volatile variable in a limited number of cases. To enable the volatile variable to provide ideal thread security, the following conditions must be met simultaneously:
- Write operations on variables do not depend on the current value.
- This variable is not included in the variant with other variables.
In fact, these conditions indicate that the valid values that can be written into the volatile variable are independent of the State of any program, including the current state of the variable. The limitation of the first condition makes the volatile variable not used as a thread security counter. Although incremental operations (x++ ) It looks like a separate operation. In fact, it is a combination of read-Modify-write operation sequences and must be executed in an atomic manner, while volatile cannot provide the required atomic features. To achieve the correct operation, you needx The value remains unchanged during the operation, while the volatile variable cannot. (However, if you adjust the value to write only from a single thread, you can ignore the first condition .) Most programming scenarios conflict with one of these two conditions, making the volatile variable notsynchronized This method is applicable to thread security. Listing 1 shows a non-thread-safe value range class. It contains a non-variant-the lower bound is always less than or equal to the upper bound. List 1. Non-thread-safe value range class
@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; }} |
This method limits the range of state variables, solower The definition of the upper field as the volatile type does not fully implement the thread security of the class; therefore, synchronization still needs to be used. Otherwise, if two threads use inconsistent values for execution at the same timesetLower AndsetUpper Otherwise, the range will be in an inconsistent state. For example, if the initial status is(0, 5) , Thread a calls at the same timesetLower(4) And thread B callssetUpper(3) Obviously, the values of the two operations do not meet the conditions, so both threads will pass the check to protect the variant, so that the final range value is(4, 3) -- An invalid value. For other operations on the scope, we needsetLower() AndsetUpper() Operation atomicity -- defining a field as the volatile type cannot achieve this goal. Performance Considerations The main reason for using the volatile variable is its simplicity: in some cases, using the volatile variable is much easier than using the corresponding lock. The secondary reason for using the volatile variable is its performance: in some cases, the volatile variable synchronization mechanism has better performance than the lock. It is difficult to make accurate and comprehensive comments, such as "X is always faster than Y", especially for internal JVM operations. (For example, in some cases, the VM may be able to completely delete the lock mechanism, which makes it difficult for us to abstract and comparevolatile Andsynchronized .) That is to say, in most of the current processor architectures, the volatile read operation overhead is very low-almost the same as the non-volatile read operation. Volatile write operations have more overhead than non-volatile write operations, because to ensure visibility, You need to implement memory definition (memory fence). Even so, the total cost of volatile is still lower than the lock acquisition. Volatile operations do not cause congestion like locks. Therefore, volatile provides some scalable features that are better than locks when volatile can be safely used. If the number of read operations far exceeds the write operation, compared with the lock, the volatile variable can usually reduce the performance overhead of synchronization. Correct volatile Mode Many concurrency experts actually tend to guide users away from volatile variables because using them is more error-prone than using locks. However, if you follow well-defined patterns with caution, you can safely use the volatile variable in many scenarios. Always remember the restrictions on Using volatile-use volatile only when the status is truly independent from other content in the program-this rule can avoid extending these patterns to insecure use cases. Mode #1: Status flag The standard usage of volatile variables may only use a Boolean status flag to indicate an important one-time event, such as initialization or request downtime. Many applications contain a control structure in the form of "execute some work when you are not ready to stop the program", as shown in Listing 2: Listing 2. Using the volatile variable as a status flag
volatile boolean shutdownRequested;...public void shutdown() { shutdownRequested = true; }public void doWork() { while (!shutdownRequested) { // do stuff }} |
It is likely to be called from outside the loopshutdown() Method -- that is, in another thread -- therefore, you need to execute some synchronization to ensure correct implementation.shutdownRequested Variable visibility. (It may be called from the JMX listener, the Operation listener in the GUI event thread, through RMI, through a Web service, etc ). Howeversynchronized The block compiling cycle is much more difficult than using the volatile status flag shown in Listing 2. Because volatile simplifies encoding and the status flag does not depend on any other State in the program, volatile is ideal for this scenario. One common feature of this type of State mark is: there is usually only one state transition;shutdownRequested Flag fromfalse Converttrue And the program stops. This mode can be extended to the status flag of the back-and-forth conversion, but can be extended only when the conversion cycle is not noticed (fromfalse Totrue , And then convertfalse ). In addition, some atomic state conversion mechanisms, such as atomic variables, are required. Mode #2: one-time Safe publication) Lack of synchronization makes visibility impossible, making it more difficult to determine when to write object references instead of primitive values. In the absence of synchronization, the updated value referenced by an object (written by another thread) may coexist with the old value in the object state. (This is the root cause of the famous double-checked-locking problem, where the object reference is read without synchronization, the problem is that you may see an updated reference, but you will still see an incomplete object through this reference ). One technique for implementing secure publishing objects is to define object reference as volatile type. Listing 3 shows an example in which the background thread loads some data from the database during the startup phase. When other code can exploit the data, it will check whether the data has been published before use. Listing 3. Using the volatile variable for one-time secure Publishing
public class BackgroundFloobleLoader { public volatile Flooble theFlooble; public void initInBackground() { // do lots of stuff theFlooble = new Flooble(); // this is the only write to theFlooble }}public class SomeOtherClass { public void doWork() { while (true) { // do some stuff... // use the Flooble, but only if it is ready if (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble); } }} |
IftheFlooble Reference is not of the volatile type,doWork() The code intheFlooble Will get an incomplete constructedFlooble . A necessary condition for this mode is that the published object must be thread-safe, or a valid immutable object (valid immutable means that the object state will never be modified after it is released ). Volatile type references can ensure the visibility of objects in the form of release. However, if the object state changes after release, additional synchronization is required. Mode #3: independent observation) Another simple mode for Safely Using volatile is to regularly "publish" the observed results for internal use of the program. For example, assume that an environmental sensor can feel the ambient temperature. A background thread may read the sensor every several seconds and update the volatile variable containing the current document. Other threads can then read this variable to view the latest temperature value at any time. Another application that uses this mode is to collect program statistics. Listing 4 shows how the identity authentication mechanism remembers the name of the last user logged on. Will be used repeatedlylastUser Reference to publish a value for other parts of the program. Listing 4. Using the volatile variable to publish multiple independent observations
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 mode is an extension of the previous mode. Publishing a value is used elsewhere in the program, but unlike publishing a one-time event, this is a series of independent events. This mode requires that the published value be valid and unchangeable-that is, the status of the value is not changed after the release. The code that uses this value must be clear that this value may change at any time. Mode #4: "volatile Bean" Mode The volatile bean mode applies to the framework that uses JavaBeans as the "honor structure. In the volatile bean mode, JavaBean is used as a group of containers with independent attributes of getter and/or setter methods. The basic principle of the volatile bean mode is that many frameworks are the holders of variable data (for exampleHttpSession But the objects in these containers must be thread-safe. In the volatile bean mode, all data members of the JavaBean are of the volatile type, and the getter and setter methods must be very common-apart from obtaining or setting corresponding attributes, they cannot contain any logic. In addition, for data members referenced by an object, the referenced object must be valid and unchangeable. (This will disable attributes with array values, because when the array reference is declaredvolatile Only references, rather than arrays, have volatile semantics ). For any volatile variable, the unchanged type or constraints cannot contain the JavaBean attribute. The example in listing 5 shows the JavaBean following the volatile bean mode: Listing 5. person objects in volatile bean Mode
@ThreadSafepublic 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; }} |
Volatile Advanced Mode The models described in the previous sections cover most of the basic use cases. Using volatile in these models is very useful and simple. This section describes a more advanced mode in which volatile provides performance or scalability advantages. The advanced mode of volatile applications is very fragile. Therefore, you must carefully prove the assumptions and these patterns are strictly encapsulated, because even small changes can damage your code! Similarly, the reason for using a more advanced volatile use case is that it can improve performance and ensure that this performance benefit is truly determined before the advanced mode is applied. You need to weigh these models and discard readability or maintainability in exchange for possible performance gains-if you do not need to improve performance (or you cannot prove that you need it through a strict test program ), this is probably a bad transaction, because you are likely to lose more than you lose, and the value you get is lower than the value you give up. Mode #5: read-write lock policies with low overhead So far, you have learned that the volatile function is not enough to implement counters. Because++x It is actually a simple combination of three operations (read, add, and storage). If multiple threads try to perform incremental operations on the Volatile counter at the same time, the update value may be lost. However, if read operations far exceed write operations, you can use internal locks and volatile variables to reduce the overhead of Public Code paths. Use the thread-safe counter shown in Listing 6synchronized Make sure that the incremental operation is atomic and usevolatile Ensure the visibility of the current result. If the update frequency is not frequent, this method can achieve better performance, because the overhead of the read path only involves volatile read operations, which is usually better than the overhead of a non-competitive lock acquisition. Listing 6. Using volatile and synchronized together to implement "low-overhead read-write locks"
@ThreadSafepublic 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++; }} |
This technology is called a "low-overhead read-write lock" because you use different synchronization mechanisms for read/write operations. Because the write operation in this example violates the first condition for Using volatile, you cannot use volatile to securely implement the counter-you must use the lock. However, you can use volatile to ensure that the current valueVisibilitySo you can use the lock to perform all the changed operations and use volatile for read-only operations. The lock only allows one thread to access the value at a time, and volatile allows multiple threads to perform read operations. Therefore, when volatile is used to ensure the read code path, it is more shared than using the lock to execute all code paths-just like a read-write operation. However, keep in mind the weakness of this pattern: if the most basic application of this pattern is exceeded, it will become very difficult to combine these two competing synchronization mechanisms. Conclusion Compared with the lock, the volatile variable is a very simple but fragile synchronization mechanism that will provide better performance and scalability than the lock in some cases. If you strictly follow the volatile usage conditions-that is, variables are truly independent from other variables and their previous values-in some cases, you can usevolatile Replacesynchronized To simplify the code. Howevervolatile The code is often more error-prone than the code that uses the lock. The mode described in this article covers the availablevolatile Replacesynchronized The most common use cases. Following these patterns (note that they must not exceed their limits) helps you securely implement most use cases and use volatile variables for better performance. |