Mainly to explain the knowledge of volatile, and easy to encounter pitfalls.

Characteristics of volatile variables

Guaranteed visibility, not atomicity:

  • When a volatile variable is written, the JMM forces the variable from the thread’s local memory to main memory.
  • This write operation invalidates the cache of volatile variables in other threads.

To disallow reordering, let’s review that there are certain rules for reordering:

  • The reorder operation does not reorder operations that have data dependencies. For example: a = 1; b=a; In this sequence of instructions, since the second operation depends on the first operation, the two operations are not reordered at compile time or processor runtime.
  • Reordering is intended to optimize performance, but the result of a single threaded program cannot be changed no matter how it is reordered. For example: a = 1; b=2; For the three operations c=a+b, the first step (a=1) and the second step (b=2) may be reordered because there is no data dependency, but the operation C =a+b will not be reordered because the final result must be c=a+b=3.

Volatile disallows order reordering rules

Using the volatile keyword to modify shared variables disallows such reordering. When volatile is used to modify a shared variable, a memory barrier is inserted into the instruction sequence at compile time to prevent a particular type of handler from reordering. Volatile also has some rules against reordering:

  • When a program performs a read or write operation on a volatile variable, all changes to the preceding operation must have been made and the results are visible to subsequent operations. The operation behind it has certainly not been done;
  • When performing instruction optimization, statements accessed on volatile variables cannot be executed after them, nor can statements following volatile variables be executed before them.

That is, when a volatile variable is executed, all statements preceding it have been executed, but all subsequent statements have not been executed. And the result of the previous statement is visible to the volatile variable and the statements that follow it.

Volatile disallows reorder analysis

For the relevant content of this part, I copied the previous article directly, not to make up the length, because some students read this article directly without reading it. In order to make each article independent, I may quote the content in the previous article.

Take a look at the following code without volatile:

class ReorderExample {

  int a = 0;

  boolean flag = false;

  public void writer(a) {

      a = 1;                   / / 1

      flag = true;             / / 2

  }

  Public void reader(a) {

      if (flag) {                / / 3

          int i =  a * a;        / / 4

          System.out.println(i);

      }

  }

}

Copy the code

Because of the reorder effect, the final output may be 0, as described in my previous article, Java Concurrent Programming Series 1- The Basics. If volatile is introduced, let’s look at the code again:

class ReorderExample {

  int a = 0;

  boolean volatile flag = false;

  public void writer(a) {

      a = 1;                   / / 1

      flag = true;             / / 2

  }

  Public void reader(a) {

      if (flag) {                / / 3

          int i =  a * a;        / / 4

          System.out.println(i);

      }

  }

}

Copy the code

In this case, volatile also has rules that prohibit reordering of instructions. The happens-before relationships established by this process can be divided into two categories:

  1. According to the rules of procedure order, 1 happens before 2; 3 happens before 4.
  2. According to the rule of volatile, 2 happens before 3.
  3. According to the transitivity rule of happens before, 1 happens before 4.

The above happens-before relationship is graphically expressed as follows:

In the figure above, the two nodes linked by each arrow represent a happens-before relationship. The black arrows indicate the rules of program order; The orange arrow represents the volatile rule; The blue arrows represent the happens-before guarantees provided after the combination of these rules.

Here, after thread A writes A volatile variable, thread B reads the same volatile variable. All shared variables that were visible to thread A before writing the volatile variable will become visible to thread B immediately after thread B reads the same volatile variable.

Volatile is not applicable

Volatile is not suitable for compound operations

Here is an example of a self-adding variable:

public class volatileTest {

    public volatile int inc = 0;

    public void increase(a) {

        inc++;

    }

    public static void main(String[] args) {

        final volatileTest test = new volatileTest();

        for(int i=0; i<10; i++){

            new Thread(){

                public void run(a) {

                    for(int j=0; j<1000; j++)

                        test.increase();

                };

            }.start();

        }

        while(Thread.activeCount()>1)  // Ensure that all previous threads are finished

            Thread.yield();

        System.out.println("inc output:" + test.inc);

    }

}

Copy the code

Test output:

inc output:8182

Copy the code

Since ince ++ is not an atomic operation and can consist of three steps: read, add, and assign, the result does not reach 10000.

The solution

USES synchronized:

public class volatileTest1 {

    public int inc = 0;

    public synchronized void increase(a) {

        inc++;

    }

    public static void main(String[] args) {

        final volatileTest1 test = new volatileTest1();

        for(int i=0; i<10; i++){

            new Thread(){

                public void run(a) {

                    for(int j=0; j<1000; j++)

                        test.increase();

                };

            }.start();

        }

        while(Thread.activeCount()>1)  // Ensure that all previous threads are finished

            Thread.yield();

        System.out.println("add synchronized, inc output:" + test.inc);

    }

}

Copy the code

USES the Lock:

public class volatileTest2 {

    public int inc = 0;

    Lock lock = new ReentrantLock();

    public void increase(a) {

        lock.lock();

        inc++;

        lock.unlock();

    }

    public static void main(String[] args) {

        final volatileTest2 test = new volatileTest2();

        for(int i=0; i<10; i++){

            new Thread(){

                public void run(a) {

                    for(int j=0; j<1000; j++)

                        test.increase();

                };

            }.start();

        }

        while(Thread.activeCount()>1)  // Ensure that all previous threads are finished

            Thread.yield();

        System.out.println("add lock, inc output:" + test.inc);

    }

}

Copy the code

USES AtomicInteger:

public class volatileTest3 {

    public AtomicInteger inc = new AtomicInteger();

    public void increase(a) {

        inc.getAndIncrement();

    }

    public static void main(String[] args) {

        final volatileTest3 test = new volatileTest3();

        for(int i=0; i<10; i++){

            new Thread(){

                public void run(a) {

                    for(int j=0; j<100; j++)

                        test.increase();

                };

            }.start();

        }

        while(Thread.activeCount()>1)  // Ensure that all previous threads are finished

            Thread.yield();

        System.out.println("add AtomicInteger, inc output:" + test.inc);

    }

}

Copy the code

All three outputs are 1000, as follows:

add synchronized, inc output:1000

add lock, inc output:1000

add AtomicInteger, inc output:1000

Copy the code

Why should a singleton double lock be volatile

Let’s take a look at the example code:

public class penguin {

    private static volatile penguin m_penguin = null;

    // Avoid initializing objects with new

    private void penguin(a) {}

    public void beating(a) {

        System.out.println("Play beans.");

    };

    public static penguin getInstance(a) {      / / 1

        if (null == m_penguin) {               / / 2

            synchronized(penguin.class{      / / 3

                if (null == m_penguin) {       / / 4

                    m_penguin = new penguin(); / / 5

                }

            }

        }

        return m_penguin;                      / / 6

    }

}

Copy the code

In the case of concurrency, if there is no volatile keyword, the problem occurs on line 5. instance = new TestInstance(); Can be broken down into 3 lines of pseudocode:

a. memory = allocate() // Allocate memory

b. ctorInstanc(memory) // Initialize the object

c. instance = memory   // Set instance to point to the address just assigned

Copy the code

When the above code is compiled and run, it may appear that it is reordered from A-B-C to A-C-B. The following problems occur in the case of multithreading. While thread A is executing line 5, thread B comes in and executes line 2. Suppose that A reorder occurs during the execution of A, that is, A and C are executed first, and B is not executed. So since thread A executes C and instance points to an address, thread B determines that instance is not null, jumps to line 6 and returns an uninitialized object.

conclusion

Volatile guarantees thread visibility and provides some order, but it does not guarantee atomicity. At the bottom of the JVM, volatile is implemented using a “memory barrier.” Looking at the assembly code generated with and without volatile, we find that with volatile, there is an additional lock prefix. The lock prefix actually acts as a memory barrier (also known as a memory barrier). The memory barrier provides three functions:

  • It ensures that instructions are not reordered before the memory barrier, nor are they reordered after the memory barrier; That is, when the memory barrier instruction is executed, all operations in front of it have been completed;
  • It forces changes to the cache to be written to main memory immediately;
  • If it is a write operation, it invalidates the corresponding cache line in the other CPU.

It also explains scenarios where volatile is not applicable and how to resolve them, and explains why the singleton pattern requires volatile.

Welcome to more like, more articles, please pay attention to the wechat public number “Lou Zai advanced road”, point to pay attention, don’t get lost ~~