Preface
Sometimes, if you use synchronization just to read and write one or two instance fields, it seems too expensive. The volatile keyword provides a lock-free mechanism for synchronous access to the instance fields. If a domain is declared as volatile, the compiler and the virtual machine know that the domain may be updated concurrently by another thread. Before talking about the volatile keyword, we need to understand the relevant concepts of the memory model and the three characteristics in concurrent programming: atomicity, visibility and orderliness.
1. Java memory model with atomicity, visibility and orderliness
The Java memory model stipulates that all variables exist in main memory, and each thread has its own working memory. All operations of a thread on a variable must be performed in working memory, and cannot directly operate on the main memory. And each thread cannot access the working memory of other threads.
In java, execute the following statement:
int i=3;
The execution thread must first assign the cache line where the variable i is located in its own work thread, and then write it to the main memory. Instead of writing the value 3 directly into the main memory.
So what guarantees does the Java language itself provide for atomicity, visibility and orderliness?
Atomicity
The read and assignment operations of variables of the basic data type are atomic operations, that is, these operations cannot be interrupted and either executed or not.
Let's take a look at the following code:
x = 10; //Statement 1y = x; //Statement 2x++; //Statement 3x = x + 1; //Statement 4
Only statement 1 is an atomic operation, and none of the other three statements are atomic operations.
Statement 2 actually contains 2 operations. It first needs to read the value of x, and then write the value of x to the working memory. Although the two operations of reading the value of x and writing the value of x to the working memory are atomic operations, they are not atomic operations together.
Similarly, x++ and x = x+1 include 3 operations: read the value of x, perform the operation of adding 1, and write the new value.
In other words, only simple reading and assignment (and the number must be assigned to a variable, and the mutual assignment between variables is not an atomic operation) is an atomic operation.
There are many classes in the java.util.concurrent.atomic package that use very efficient machine-level instructions (rather than locks) to ensure the atomicity of other operations. For example, the AtomicInteger class provides methods incrementAndGet and decrementAndGet, which respectively increase and decrease an integer in atomic manner. The AtomicInteger class can be safely used as a shared counter without synchronization.
In addition, this package also contains atomic classes such as AtomicBoolean, AtomicLong and AtomicReference for system programmers who develop concurrent tools only, and application programmers should not use these classes.
Visibility
Visibility refers to the visibility between threads, and the modified state of one thread is visible to another thread. That is the result of a thread modification. Another thread will be seen immediately.
When a shared variable is modified by volatile, it ensures that the modified value will be updated to main memory immediately, so it is visible to other threads. When other threads need to read, it will read the new value in memory.
However, ordinary shared variables cannot guarantee visibility, because it is uncertain when the normal shared variable is written to the main memory after it is modified. When other threads read it, the original old value may still be in the memory, so visibility cannot be guaranteed.
Orderful
In the Java memory model, compilers and processors are allowed to reorder instructions, but the reordering process will not affect the execution of single-threaded programs, but will affect the correctness of multi-threaded concurrent execution.
The volatile keyword can be used to ensure a certain "orderline". In addition, synchronized and Lock can be used to ensure order. Obviously, synchronized and Lock ensure that there is a thread that executes synchronization code at each moment, which is equivalent to letting threads execute synchronization code in sequence, which naturally ensures order.
2. volatile keywords
Once a shared variable (class member variables, class static member variables) is modified by volatile, it has two layers of semantics:
Let’s look at a piece of code first. If thread 1 is executed first and thread 2 is executed later:
//Thread 1boolean stop = false; while(!stop){ doSomething();}//Thread 2stop = true; Many people may use this markup method when interrupting threads. But in fact, will this code run completely correctly? Will the thread be interrupted? Not necessarily. Perhaps most of the time, this code can interrupt threads, but it may also cause the thread to not be interrupted (although this possibility is very small, once this happens, it will cause a dead loop).
Why is it possible to cause thread failure to interrupt? Each thread has its own working memory during its running process. When thread 1 is running, it will copy the value of the stop variable and put it in its own working memory. Then when Thread 2 changes the value of the stop variable, but has not had time to write it to the main memory, Thread 2 goes to do other things, then Thread 1 does not know about Thread 2's changes to the stop variable, so it will continue to loop.
But after modifying with volatile it becomes different:
Does volatile guarantee atomicity?
We know that the volatile keyword ensures the visibility of operations, but can volatile ensure that the operations on variables are atomic?
public class Test { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } // Ensure that the previous threads have completed execution while(Thread.activeCount()>1) Thread.yield(); System.out.println(test.inc); }} The result of this code is inconsistent every time it runs. It is a number less than 10,000. As mentioned earlier, the auto-increment operation is not atomic. It includes reading the original value of a variable, performing an additional 1 operation, and writing to working memory. That is to say, the three sub-operations of the self-increment operation may be executed separately.
If the value of variable inc is 10 at a certain time, thread 1 performs self-increment operation on the variable, thread 1 first reads the original value of variable inc, and then thread 1 is blocked; then thread 2 performs self-increment operation on the variable, and thread 2 also reads the original value of variable inc. Since thread 1 only performs a read operation on variable inc and does not modify the variable, it will not cause the cache line of cache variable inc in thread 2 to be invalid in the working memory, so thread 2 will directly go to the main memory to read the value of inc. When it is found that the value of inc is 10, then performs an increase of 1, and writes 11 to the working memory, and finally writes it to the main memory. Then thread 1 then performs the addition operation. Since the value of inc has been read, note that the value of inc in thread 1 is still 10 at this time, so after thread 1 adds inc, the value of inc is 11, then writes 11 to work memory, and finally writes it to main memory. Then after the two threads perform a self-increment operation, inc only increases by 1.
The autoincrement operation is not an atomic operation, and volatile cannot guarantee that any operation on a variable is atomic.
Can volatile ensure orderliness?
As mentioned earlier, the volatile keyword can prohibit instruction reordering, so volatile can ensure order to a certain extent.
There are two meanings forbidden reordering of volatile keywords:
3. Use the volatile keyword correctly
Synchronized keyword prevents multiple threads from executing a piece of code at the same time, which will greatly affect the program execution efficiency. The performance of the volatile keyword is better than synchronized in some cases. However, it should be noted that the volatile keyword cannot replace the synchronized keyword, because the volatile keyword cannot guarantee the atomicity of the operation. Generally speaking, the following two conditions must be met when using volatile:
The first condition is that it cannot be operations such as self-increase and self-decrease. As mentioned above, volatile does not guarantee atomicity.
Let's give an example of this. It contains an invariant: the lower bound is always less than or equal to the upper bound.
public class NumberRange { private volatile 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 way limits the scoped state variables, so defining the lower and upper fields as volatile types does not fully implement the class's thread safety, and thus still requires synchronization. Otherwise, if two threads happen to execute setLower and setUpper with inconsistent values at the same time, the range will be inconsistent. For example, if the initial state is (0, 5), at the same time, thread A calls setLower(4) and thread B calls setUpper(3), it is obvious that the values stored in the cross-stored by these two operations do not meet the conditions, then both threads will pass the check to protect the invariant, so that the last range value is (4, 3), which is obviously wrong.
In fact, it is to ensure the atomicity of the operation to use volatile. There are two main scenarios for using volatile:
Status flags
volatile boolean shutdownRequested;...public void shutdown() { shutdownRequested = true; }public void doWork() { while (!shutdownRequested) { // do stuff }}It is 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. However, writing a loop with synchronized blocks is much more troublesome than writing with volatile status flags. Because volatile simplifies encoding and the status flags do not depend on any other state in the program, it is very suitable for volatile here.
Double check mode (DCL)
public class Singleton { private volatile static Singleton instance = null; public static Singleton getInstance() { if (instance == null) { synchronized(this) { if (instance == null) { instance = new Singleton(); } } return instance; } } Using volatile here will more or less affect performance, but considering the correctness of the program, it is still worth sacrificing this performance.
The advantage of DCL is that it has high resource utilization. Singleton objects are instantiated only when the getInstance is executed for the first time, which is highly efficient. The disadvantage is that the reaction is slightly slower when loading for the first time, and there are certain defects in high concurrency environments, although the probability of occurrence is very small.
Although DCL solves the problems of resource consumption, unnecessary synchronization, thread safety, etc. to a certain extent, it still has failure problems in some cases, that is, DCL failure. In the book "Java Concurrency Programming Practice", it recommends using the following code (static internal class singleton pattern) to replace DCL:
public class Singleton { private Singleton(){ } public static Singleton getInstance(){ return SingletonHolder.sInstance; } private static class SingletonHolder { private static final Singleton sInstance = new Singleton(); } } About double checks, you can view
4. Summary
Compared to locks, the Volatile variable is a very simple but at the same time very fragile synchronization mechanism that will in some cases provide better performance and scalability than locks. If you strictly follow the usage conditions of volatile that are truly independent of other variables and their own previous values, you can use volatile instead of synchronized to simplify the code in some cases. However, code using volatile is often more prone to errors than code using locks. This article introduces two most common use cases where volatile can be used instead of synchronized. In other cases, we'd better use synchronized.
The above is all about this article, I hope it will be helpful to everyone's learning.