Learn the volatile domain of Java multithreading

  • 2020-05-05 11:17:07
  • OfStack

Preface to

Sometimes using synchronization just to read and write one or two instance domains can be too expensive, and the volatile keyword provides a lock-free mechanism for synchronized access to the instance domain. If a domain is declared volatile, the compiler and virtual machine know that the domain is likely to be updated concurrently by another thread. Before moving on to the volatile keyword we need to understand the concepts of the memory model and the three features of concurrent programming: atomicity, visibility, and orderliness.

1. java memory model with atomicity, visibility and orderliness

The Java memory model states that all variables are stored in main memory and that each thread has its own working memory. All operations of a thread on a variable must be performed in working memory, not directly on main memory. And each thread cannot access the working memory of other threads.
In java, execute the following statement:


int i=3;

The thread of execution must first assign a value to the cached row of the variable i in its worker thread before writing to main memory. Instead of writing the number 3 directly into main memory.
So what guarantees does Java itself provide for atomicity, visibility, and orderliness?

atomicity

Reads and assignments to variables of basic data types are atomic operations, meaning that they are not interruptible and are either performed or not performed.
Take a look at the following code:


x = 10;    // statements 1
y = x;     // statements 2
x++;      // statements 3
x = x + 1;   // statements 4

Only statement 1 is an atomic operation, and none of the other three statements are atomic operations.
Statement 2 actually consists of two operations that read x and then write x to working memory. Although reading x and writing x to working memory are atomic operations, they are not altogether atomic operations.
Similarly, x++ and x = x+1 include three operations: read the value of x, add 1, and write the new value.
That is, only simple reads and assignments (and must be numeric assignments to a variable, and mutual assignments between variables are not atomic operations) are atomic operations.
java. util. concurrent. atomic package has a lot of classes use the very efficient machine-level instructions (instead of using the lock) to ensure that the atomicity of other operations. The AtomicInteger class, for example, provides the methods incrementAndGet and decrementAndGet, which atomically add and subtract an integer, respectively. You can safely use the AtomicInteger class as a Shared counter without synchronization.
The package also contains the AtomicBoolean, AtomicLong, and AtomicReference atomic classes for use only by system programmers developing concurrency tools, and should not be used by application programmers.

visibility

Visibility refers to the visibility between threads, the state that one thread modifies is visible to another thread. This is the result of a thread modification. The other thread will see it in a second.
When a Shared variable is modified by volatile, it guarantees that the modified value will be immediately updated to main memory, so it is visible to other threads, and when there are other threads to read, it will go to memory to read the new value.
However, common Shared variables cannot guarantee visibility, because when common Shared variables are written into main memory after being modified is not certain, when other threads read, the memory may still be the original old value, so visibility cannot be guaranteed.

orderliness

In the Java memory model, the compiler and processor are allowed to resort instructions, but the resort process does not affect the execution of single-threaded programs, but the correctness of concurrent execution of multiple threads.
Some "order" can be ensured by the volatile keyword. In addition, synchronized and Lock can be used to guarantee the orderliness. Obviously, synchronized and Lock guarantee that there is one thread executing the synchronized code at every moment, which is equivalent to letting the threads execute the synchronized code sequentially, thus guaranteeing the orderliness.

2. volatile keyword

Once a Shared variable (a member of a class, a static member of a class) is modified by volatile, there are two levels of semantics:

guarantees visibility when different threads operate on this variable, meaning that one thread modifies the value of a variable, and the new value is immediately visible to other threads. prohibits instruction reordering.

Look at a piece of code first, if thread 1 executes first, thread 2 executes after:


// thread 1
boolean stop = false;
while(!stop){
  doSomething();
}

// thread 2
stop = true;

Many people will probably use this notation when they interrupt a thread. But in fact, will this code work exactly right? Does it have to interrupt the thread? Not necessarily, perhaps most of the time, this code will be able to interrupt the thread, but it may also cause the thread to be uninterruptible (which is a small possibility, but it will cause a dead loop if this happens).
Why is it possible to fail to interrupt a thread? Each thread has its own working memory during the run, so when thread 1 runs, it will copy the value of stop variable and put it in its own working memory. So when thread 2 changes the value of stop, but before it can write into main memory, thread 2 moves on to do something else, so thread 1 doesn't know what thread 2 has done to stop, so it keeps going.
But with volatile, it's a different story:

USES the volatile keyword to force the modified value to be written into main memory immediately. If USES volatile keyword, when thread 2 is modified, the cache line of the cache variable stop in thread 1's working memory will be invalid. because the cache line of the cache variable stop is invalid in thread 1's working memory, thread 1 reads the value of the variable stop from main memory when it reads it again.

Does volatile guarantee atomicity?

We know that the volatile keyword guarantees visibility of operations, but can volatile guarantee atomicity of operations on variables?


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 all previous threads are executed 
    while(Thread.activeCount()>1) 
      Thread.yield();
    System.out.println(test.inc);
  }
}

This code is inconsistent every time it is run, is a number less than 10000, has been mentioned before, the operation is not atomic, it includes reading the original value of the variable, doing 1 operation, writing to the working memory. So the three suboperations of the self-increment operation may be split.
If at some point the value of the variable inc is 10, thread 1 increments the variable, thread 1 reads the original value of the variable inc, and thread 1 is blocked. Then thread 2 variables for the operation, thread 2 also went to read the original value of the variable inc, because thread 1 just to read operations, variable inc without modify variables, so will not lead to thread 2 working memory cache variable inc cache line is invalid, so the thread 2 will go directly to main memory read inc values, when he found the value of the inc 10, then add 1 operation, and put into the working memory, the last write into main storage. Since the inc value has been read, notice that the inc value is still 10 in the working memory of thread 1, so the inc value is 11 after thread 1 adds 1 to inc, then writes 11 to the working memory, and finally writes to main memory. So after the two threads have each done a self-increment operation, inc only increases by 1.
The autoincrement operation is not atomic, and volatile cannot guarantee that any operation on a variable is atomic.

Does volatile guarantee orderliness?

As mentioned earlier, the volatile keyword disallows instruction reordering, so volatile guarantees ordering to some extent.
The volatile keyword prohibition instruction reorder has two meanings:

when a program performs a read or write operation to the volatile variable, all changes to the previous operation must have been made, and the result is visible to the subsequent operation. The operation behind it has certainly not been carried out; When performing instruction optimization, cannot be followed by statements accessing volatile variables, nor can statements following volatile variables be followed by them.

3. Use the volatile keyword

correctly

The synchronized keyword prevents multiple threads from executing a piece of code at the same time, which can affect program execution efficiency. The volatile keyword performs better than synchronized in some cases. However, it is important to note that the volatile keyword cannot replace the synchronized keyword, because the volatile keyword cannot guarantee the atomicity of the operation. Generally speaking, volatile requires the following two requirements:

writes to a variable independent of the current value the variable is not included in an invariant with other variables

The first condition is that it cannot be self-increasing or self-decreasing. As mentioned above, volatile does not guarantee atomicity.
The second condition, for example, 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 approach limits the scope's state variables, so defining the lower and upper fields as volatile types does not fully implement the thread-safety of the class, so synchronization is still required. Otherwise, if two threads happen to execute setLower and setUpper at the same time with inconsistent values, the scope will be in an inconsistent state. For example, if the initial state is (0, 5), and the thread A calls setLower(4) at the same time, and the thread B calls setUpper(3), it is clear that the values stored across the two operations are not qualified, then both threads will pass the check to protect the invariant so that the final range value is (4, 3), which is obviously not correct.
volatile can be used to ensure atomicity of operations. There are two main scenarios for volatile :

status flag


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 -- that is, in another thread -- so some sort of synchronization needs to be performed to ensure the visibility of the shutdownRequested variable is correctly implemented. However, writing a loop with the synchronized block is more cumbersome than writing with the volatile status flag. Because volatile simplifies coding and the status flag does not depend on any other state in the program, volatile is a good place to use.

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 have a slight impact on performance, but considering the correctness of the program, it's worth the sacrifice.
DCL has the advantage of high resource utilization. The singleton is not instantiated until getInstance is executed for the first time, which is efficient. The disadvantage is that the first load reaction is a little bit slower, in the high concurrency environment also has certain defects, although the probability of occurrence is very small.
Although DCL solves the problems of resource consumption, redundant synchronization and thread safety to some extent, it still fails in some cases, that is, DCL fails. In the book concurrent programming practice of java, it is recommended to replace DCL with the following code (static inner class singleton mode) :


public class Singleton { 
  private Singleton(){
  }
   public static Singleton getInstance(){ 
    return SingletonHolder.sInstance; 
  } 
  private static class SingletonHolder { 
    private static final Singleton sInstance = new Singleton(); 
  } 
} 

For double checking, see

4. Summary

Compared to locks, the Volatile variable is a very simple but at the same time very fragile synchronization mechanism, which in some cases will provide better performance and scalability than locks. In some cases, volatile can be used instead of synchronized to simplify the code, if the use condition of volatile is strictly followed, that is, the variable is truly independent of other variables and its own previous values. However, code using volatile is often more error-prone than code using locks. This article describes two of the most common use cases where volatile can be used instead of synchronized, and other cases where synchronized is best used.

The above is the entire content of this article, I hope to help you with your study.


Related articles: