What is your understanding of volatile?

Do you know the underlying implementation mechanism for volatile?

What is the difference between volatile variables and atomic variables?

Can you give two examples of how volatile is used?

GitHub JavaKeeper, contains N line Internet development skills essential weapon spectrum

The Java memory model (JMM) is built around how concurrency handles visibility, atomicity, and orderliness. Volatile guarantees two of these properties.

1. The concept

Volatile is the Java keyword. It is a variable modifier used to modify variables that can be accessed and modified by different threads.


2. Three features of the Java memory model

2.1 the visibility

Visibility is a complex property because errors in visibility often go against our intuition. Often, there is no guarantee that the thread doing the read will see the value written by the other thread in time, sometimes even impossible. To ensure visibility of memory writes between multiple threads, a synchronization mechanism must be used.

Visibility refers to the visibility between threads, where the modified state of one thread is visible to another thread. That’s the result of a thread modification. Another thread will see that in a second.

Volatile, synchronized, and final are all visible in Java.

2.2 atomic

Atomicity means that when a thread is performing an operation, the middle cannot be blocked or split, and the whole thread either succeeds or fails. Such as a = 0; (a non-long and double) The operation is indivisible, so we say the operation is atomic. Another example: a++; This operation is actually a = a + 1; It’s divisible, so it’s not an atomic operation. All non-atomic operations have thread-safety issues, requiring sychronization to make it an atomic operation. If an operation is atomic, it is said to be atomic. Java provides several atomic classes under the Concurrent package, AtomicInteger, AtomicLong, AtomicReference, and so on.

Synchronized and lock and unlock ensure atomicity in Java.

2.3 order

The Java language provides the keywords volatile and synchronized to ensure order between threads. Volatile because it contains the semantics of “forbid instruction reordering.” Synchronized is acquired by the rule that a variable can only be locked by one thread at a time, which determines that two synchronized blocks holding the same object lock can only be executed serially.


3. Volatile is a lightweight synchronization mechanism provided by the Java virtual machine

  • Guaranteed visibility
  • Atomicity is not guaranteed
  • Disallow instruction reordering (to ensure orderliness)

3.1 Empty claim without proof, code verification

3.1.1 Visibility verification

class MyData {
    int number = 0;
    public void add(a) {
        this.number = number + 1; }}// Start two threads, a work thread and a main thread, and check the number of the main thread after the work thread changes the number
   private static void testVolatile(a) {
        MyData myData = new MyData();
     
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\t come in");
            try {
                TimeUnit.SECONDS.sleep(2);
                myData.add();
                System.out.println(Thread.currentThread().getName()+"\t update number value :"+myData.number);
            } catch(InterruptedException e) { e.printStackTrace(); }},"workThread").start();

        // The second thread, main thread
        while (myData.number == 0) {// The main thread is still looking for 0
        }
        System.out.println(Thread.currentThread().getName()+"\t mission is over");      
        System.out.println(Thread.currentThread().getName()+\t mission is over, main get number is:+myData.number); }}Copy the code

Run testVolatile() and the output is as follows, indicating that the main thread is always zero

workThread	 execute
workThread	 update number value :1
Copy the code

Volatile int number = 0; volatile int number = 0; volatile int number = 1

WorkThread execute workThread Update number Value :1 Main execute over, main get number is:1Copy the code

3.1.2 Atomicity verification is not guaranteed

class MyData {
    volatile int number = 0;
    public void add(a) {
        this.number = number + 1; }}private static void testAtomic(a) throws InterruptedException {
  MyData myData = new MyData();

  for (int i = 0; i < 10; i++) {
    new Thread(() ->{
      for (int j = 0; j < 1000; j++) { myData.addPlusPlus(); }},"addPlusThread:"+ i).start();
  }


  // Wait until the top 20 threads have finished (which is expected to end in 5 seconds), and get the last number in the main thread
  TimeUnit.SECONDS.sleep(5);
  while (Thread.activeCount() > 2){
    Thread.yield();
  }
  System.out.println("The final value."+myData.number);
}
Copy the code

Running testAtomic finds that the final output value, not necessarily the expected value of 10000, is often less than 10000.

Final value: 9856Copy the code

Why is this so, because i++ has four instructions when converted to bytecode instructions

  • getfieldGet the original value
  • iconst_1The value into the stack
  • iaddAdd one
  • putfieldiaddSubsequent operations are written back to main memory

This can lead to multithreaded race problems at runtime, with the possibility of losing write values.

How do you solve the atomicity problem?

Add synchronized or simply use the Automic atomic class.

3.1.3 Disabling instruction rearrangement verification

When a computer executes a program, to improve performance, the compiler and processor often rearrange instructions, generally divided into the following three types

The processor must consider the data dependencies between instructions when reordering, which we call as-IF-serial semantics

In a single-threaded environment, ensure that the final execution of the program is consistent with the sequential execution of the code; However, in a multi-threaded environment, threads are executed alternately. Due to the existence of compiler optimization rearrangement, it is uncertain whether the variables used in the two threads can guarantee consistency, and the results are unpredictable.

We often use the following code to verify that volatile disallows instruction rearrangements. If the output in a multithreaded environment is not necessarily the expected 2, set both variables to volatile.

public class ReSortSeqDemo {

    int a = 0;
    boolean flag = false;

    public void mehtod1(a){
        a = 1;
        flag = true;
    }

    public void method2(a){
        if(flag){
            a = a +1;
            System.out.println("reorder value: "+a); }}}Copy the code

Volatile prevents instruction reordering optimization and thus prevents out-of-order execution of programs in multithreaded environments.

In addition, the singleton mode of the DCL(Double-Checked Locking) version, which is the most common multithreaded environment, uses volatile to prohibit instruction reordering.

public class Singleton {

    private static volatile Singleton instance;
  
    private Singleton(a){}
    // DCL
    public static Singleton getInstance(a){
        if(instance ==null) {// First check
            synchronized (Singleton.class){
                if(instance == null) {// Second check
                    instance = newSingleton(); }}}returninstance; }}Copy the code

Because of instruction reordering, the two-ended retrieval mechanism is not necessarily thread-safe.

why ?

Because: instance = new Singleton(); The process of initializing an object is not actually an atomic operation, it is performed in three parts,

  1. Allocate memory to instance
  2. The instance constructor is called to initialize the object
  3. Reference instance to the allocated memory space (instance is not null after this step)

Step 2 and Step 3 do not depend on each other. If the VM has instruction reordering optimization, the sequence of Step 2 and Step 3 cannot be determined. If thread A enters the synchronized block first and executes 3 instead of 2, instance is already non-null. When thread B first checks for instance, it will find that instance is already non-null and return it to use, but the instance has not been initialized yet. So we restrict the instruction rearrangement of the instance object and use the volatile modifier. (Prior to JDK 5, the use of volatile double-checking was problematic.)


Principle 4.

Volatile guarantees thread visibility and provides some order, but not atomicity. The underlying JVM implementation is based on memory barriers.

  • When reading or writing non-volatile variables, each line first copies the variables from memory to the CPU cache. If the computer has multiple cpus, each thread may be processed on a different CPU, which means that each thread can be copied to a different CPU cache
  • Declared variables are volatile, and the JVM guarantees that each read is read from memory, skipping the CPU cache, so there are no visibility issues
    • A write to a volatile variable is followed by a store barrier instruction, flushing the shared variable from working memory back to main memory.
    • A read on a volatile variable is followed by a load barrier instruction that reads the shared variable from main memory.

Use the HSDIS tool to get the assembly instructions generated by the JIT compiler to see what the CPU will do if it writes to volatile, or use the singleton pattern above, as you can see

(PS: The assembly instruction is a bit too far down the road for my Javaer, but the JVM bytecode we can see that putstatic means to set the value of a static variable. I’m more sure I’m assigning instance. Lock ADD1 lock ADD1 lock ADD1 lock ADD1 Here you can see the two articles in www.jianshu.com/p/6ab7c3db1… , www.cnblogs.com/xrq730/p/70.)

Volatile shared variables are written with a second line of assembly code that adds zero to the original value, where the addl instruction is preceded by the lock modifier. According to the IA-32 architecture software developer’s manual, the lock prefixed instruction causes two things on a multicore processor:

  • Writes data from the current processor cache row back to system memory
  • This write back to memory invalidates data cached in other cpus

It is lock that implements the “prevent instruction reordering” and “memory visibility” features of volatile


5. Application scenarios

You can use volatile variables instead of locks only in a limited number of cases. For volatile variables to provide ideal thread-safety, both conditions must be met:

  • Writes to variables do not depend on the current value
  • This variable is not contained in an invariant with other variables

In situations where atomicity is required, do not use volatile.


6. Volatile performance

Volatile has almost the same read cost as normal variables, but writes are slower because it requires inserting many memory-barrier instructions into the native code to keep the processor from executing out of order.

To quote from “Using the Volaitle Variable correctly” :

It is difficult to make an accurate, comprehensive assessment, such as “X is always faster than Y,” especially for operations within the JVM. (For example, in some cases the JVM may be able to remove locking entirely, making it difficult to compare the overhead of volatile and synchronized in an abstract way.) That is, on most processor architectures today, volatile reads are very cheap — almost as expensive as non-volatile reads. Volatile writes are much more expensive than non-volatile writes because the Memory Fence is required to ensure visibility, but even so, the total cost of volatile writing is lower than lock acquisition.

Volatile operations do not block like locks, so volatile can provide some scalability advantages over locks when they can be used safely. If reads far outnumber writes, volatile variables can often reduce the performance cost of synchronization compared to locking.

reference

The deep understanding of the Java virtual machine “tutorials.jenkov.com/java-concur… Juejin. Cn/post / 684490… “Using Volatile Variables correctly” www.ibm.com/developerwo…