The volatile variable provides thread visibility and does not guarantee thread safety and atomicity.
What is thread visibility:
Locks provide two main features: mutual exclusion and visibility. Mutual exclusion means that only one thread can hold a specific lock at a time, so this feature can be used to implement a coordinated access protocol for shared data, so that only one thread can use the shared data at a time. Visibility is a little more complex, and it must ensure that changes to the shared data before releasing the lock are visible to another thread that subsequently acquires the lock - without this visibility guarantee provided by the synchronization mechanism, the shared variables seen by the thread may be pre-modified or inconsistent values, which will cause many serious problems.
See the semantics of volatile :
volatile is equivalent to a weak implementation of synchronized, which means that volatile implements synchronized semantics, but does not have a lock mechanism. It ensures that updates to the volatile field inform other threads in a predictable way.
volatile contains the following semantics:
(1) The Java storage model will not reorder the operations of valatile instructions: This ensures that operations on volatile variables are executed in the order in which the instructions appear.
(2) The volatile variable will not be cached in the register (only the threads are visible) or other places that are not visible to the CPU. The result of the volatile variable is always read from the main memory every time. In other words, for the modification of the volatile variable, other threads are always visible, and they are not using variables inside the thread stack. That is, in the happens-before law, after writing an valatile variable, any subsequent read operation can be understood to see the result of this write operation.
Although the characteristics of the volatile variable are good, volatile cannot guarantee thread safety. That is to say, the operation of the volatile field is not atomic. The volatile variable can only guarantee visibility (other threads can understand the results after seeing this change after one thread is modified). To ensure atomicity, you can only lock it so far!
Principles for using Volatile:
Three principles for applying volatile variables:
(1) Write variables do not depend on the value of this variable, or only one thread modifys this variable
(2) The state of the variable does not need to participate in the invariant constraints with other variables
(3) Access variables do not need to be locked
In fact, these conditions indicate that these valid values that can be written to the volatile variable are independent of the state of any program, including the current state of the variable.
The first condition limits prevent the volatile variable from being used as a thread-safe counter. Although the incremental operation (x++) looks like a separate operation, it is actually a combined operation composed of a sequence of read-modify-write operations that must be performed atomically, and volatile cannot provide the necessary atomic properties. Implementing the correct operation requires keeping the value of x constant during the operation, which is not possible with the volatile variable. (However, if the value is adjusted to be written only from a single thread, the first condition can be ignored.)
Most programming situations conflict with one of these three conditions, making the volatile variable not as universally applicable to thread safety as synchronized. Listing 1 shows a non-thread-safe numerical range class. It contains an invariant - the lower bound is always less than or equal to the upper bound.
Use volatile correctly:
Mode #1: Status Flags
Perhaps the specification of implementing the volatile variable is simply to use a Boolean status flag to indicate that an important one-time event has occurred, such as completing initialization or requesting downtime.
Many applications include a control structure in the form of "execute some work when the program is not ready to stop", as shown in Listing 2:
Listing 2. Use the volatile variable as a status flag
volatile boolean shutdownRequested; … public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } }It is very likely that the shutdown() method will be called from outside the loop - i.e. in another thread - so some kind of synchronization needs to be performed to ensure the visibility of the shutdownRequested variable is correctly implemented. (May be called from JMX listener, operation listener in GUI event thread, through RMI, through a web service, etc.). However, writing a loop with a synchronized block is much more troublesome than writing with the volatile status flag shown in Listing 2. Because volatile simplifies encoding and the status flags do not depend on any other states within the program, it is very suitable for volatile here.
A common feature of this type of state tag is that there is usually only one state transition; the shutdownRequested flag is converted from false to true, and the program stops. This pattern can be extended to the status flag of the back and forth transition, but can only be extended if the transition period is not noticed (from false to true, then to false). In addition, some atomic state conversion mechanisms are required, such as atomic variables.
Mode #2: One-time safe publication
Lack of synchronization can lead to unachievable visibility, which makes it more difficult to determine when to write an object reference instead of a primitive value. In the absence of synchronization, an updated value referenced by an object (written by another thread) may be encountered and the old value of the state of that object exists simultaneously. (This is the root cause of the famous double-checked-locking problem where object references are read without synchronization, resulting in the problem that you may see an updated reference, but still see an incompletely constructed object through that reference).
One technique to implement safe publishing of objects is to define object references as volatile type. Listing 3 shows an example where the background thread loads some data from the database during startup. Other codes, when they are able to utilize this data, check whether it has been published before use.
Listing 3. Using the volatile variable for a one-time safe release
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); } } }If theFlooble reference is not a volatile type, the code in doWork() will get an incompletely constructed Flooble when it unreferences theFlooble.
A necessary condition for this pattern is that the published object must be thread-safe, or a valid immutable object (effective immutable means that the state of the object will never be modified after publication). References of type volatile ensure visibility of the object's publication form, but additional synchronization is required if the object's state will change after publication.
Pattern #3: Independent observation
Another simple mode of using volatile safely is to "release" observations regularly for internal use of the program. For example, suppose there is an ambient sensor capable of sensing the ambient temperature. A background thread may read the sensor every few seconds and update the volatile variable containing the current document. Then, other threads can read this variable so that they can see the latest temperature value at any time.
Another application that uses this mode is to collect the program's statistics. Listing 4 shows how the authentication mechanism remembers the name of the user who was logged in for the last time. Repeat the lastUser reference to publish values for other parts of the program.
Listing 4. Using the volatile variable for publishing 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 certain value for use elsewhere in the program, but unlike publishing a one-time event, it is a series of independent events. This pattern requires that the published value is valid and immutable -- that is, the status of the value will not change after publication. The code that uses this value needs to be clear that the value may change at any time.
Mode #4:"volatile bean" mode
The volatile bean pattern is suitable for frameworks that use JavaBeans as a "honor structure". In the volatile bean pattern, JavaBeans are used as a set of containers with independent properties of getter and/or setter methods. The basic principle of the volatile bean pattern is that many frameworks provide containers for holders of volatile data (such as HttpSession), but the objects placed in these containers must be thread-safe.
In volatile bean mode, all data members of a JavaBean are of volatile type, and the getter and setter methods must be very ordinary - they cannot contain any logic except to obtain or set corresponding properties. In addition, for the data members referenced by the object, the referenced object must be valid and immutable. (This will prohibit properties with array values, because when an array reference is declared as volatile, only the reference and not the array itself have volatile semantics). For any volatile variable, invariants or constraints cannot contain JavaBean properties. The examples in Listing 5 show JavaBeans that adhere to the volatile bean pattern:
Mode #4:"volatile bean" mode
@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; } }Advanced Mode of volatile
The patterns described in the previous sections cover most of the basic use cases, and using volatile in these patterns is very useful and simple. This section introduces a more advanced mode in which volatile will provide performance or scalability advantages.
The advanced modes of volatile applications are very fragile. Therefore, the assumptions must be carefully proven, and these patterns are strictly encapsulated because even very small changes can corrupt your code! Similarly, the reason for using a more advanced volatile use case is that it can improve performance, ensuring that you really determine that you need to achieve this performance benefit before you start applying the advanced patterns. There are trade-offs on these patterns, giving up readability or maintainability in exchange for possible performance gains--if you don't need performance improvements (or can't prove you need it through a strict test program), then it's likely to be a bad deal because you'll likely lose less money and get something worth less than what you give up.
Mode #5: Read-write lock strategy with low overhead
So far, you should understand that volatile is not capable enough to implement counters. Because ++x is actually a simple combination of three operations (read, add, store), if multiple threads happen to try to perform incremental operations on the volatile counter at the same time, its updated value may be lost.
However, if the read operation is much more than the write operation, you can use an internal lock and volatile variables to reduce the overhead of the public code path. The thread-safe counters shown in Listing 6 use synchronized to ensure that the incremental operations are atomic and volatile to ensure visibility of the current result. This method can achieve better performance if updates are not frequent, because the overhead of read paths involves only the volatile read operation, which is usually better than the overhead of a competition-free lock acquisition.
Listing 6. Use volatile and synchronized to achieve "lower overhead read-write locks"
@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 technique is called a "lower overhead read-write lock" is because you use a different synchronization mechanism for read-write operations. Because the write operation in this example violates the first condition of using volatile, the counter cannot be implemented safely with volatile - you must use a lock. However, you can use volatile in read operations to ensure visibility of the current value, so you can use locks to perform all changes and read-only with volatile. Among them, the lock allows only one thread to access the value at a time, and volatile allows multiple threads to perform read operations. Therefore, when using volatile to ensure that the code path is read, it is more sharing than using the lock to execute all code paths - just like a read-write operation. However, keep in mind the weaknesses of this model in mind: if the most basic application of this model is beyond, it will become very difficult to combine these two competing synchronization mechanisms.
About instruction reordering and the Happens-before rule
1. Let reorder
The Java language specification stipulates that the JVM thread maintains sequential semantics internally, that is, as long as the final result of the program is equivalent to its results in a strict sequential environment, the execution order of instructions may be inconsistent with the order of the code. This process is reordered by a command. The significance of instruction reordering is that the JVM can appropriately reorder machine instructions according to the characteristics of the processor (CPU's multi-level cache system, multi-core processor, etc.), so that the machine instructions are more in line with the execution characteristics of the CPU and maximize the performance of the machine.
The simplest model for program execution is to execute in the order in which the instructions appear, which is independent of the CPU that executes the instructions, ensuring the portability of the instructions to the greatest extent. The professional term for this model is called the sequential consistency model. However, modern computer systems and processor architectures do not guarantee this (because artificial designation cannot always guarantee compliance with CPU processing characteristics).
2. Appens-before rule
The Java storage model has a happens-before principle, that is, if action B wants to see the execution result of action A (regardless of whether A/B is executed in the same thread), then A/B needs to satisfy the happens-before relationship.
Before introducing the happens-before rule, introduce a concept: JMM action (Java Memeory Model Action), Java stores model actions. An action includes: read and write variables, monitor locking and release locks, thread start() and join(). The lock will be mentioned later.
happens-before complete rules:
(1) Each Action in the same thread happens-before to any Action that appears after it.
(2) Unlocking a monitor happens-before to each subsequent lock on the same monitor.
(3) Write operation to the volatile field happens-before to each subsequent read operation of the same field.
(4) The call to Thread.start() will happens-before the actions in the startup thread.
(5) All actions in Thread happens-before checks on other threads to end this thread or return in Thread.join() or Thread.isAlive()==false.
(6) One thread A calls the interrupt() of another thread B happens-before when thread A finds that B is interrupted by A (B throws an exception or A detects B isInterrupted() or interrupted()).
(7) The end of an object constructor happens-before and the beginning of the finalizer of the object
(8) If A action happens-before is in action B, and B action happens-before and C action, then A action happens-before is in action C.
The above is all about this article. I will introduce it to you here. I hope it will be helpful for you to learn and understand volatile variables in Java.