One, foreword

We’ve looked at the characteristics of the Synchronized keyword: atomicity, visibility, orderliness, reentrancy! Although the JDK is constantly trying to optimize the built-in lock, as described in Advance: no lock -> bias lock -> light lock -> weight lock there are four states, but in the case of high concurrency, it will eventually expand to weight lock.

Similarly, we mention in Passing the keyword volatile, which differs from Synchronized: Volatile is not atomic! Note: Just because it is not atomic does not mean it is not primordial!

Why do you say so? That’s because Synchronized is a Synchronized block of code that performs an atomic operation on the entire block of code through the monitor by determining the ACC_SYNCHRONZED flag bit for the entire method. Volatile is atomic for single operations and nonatomic for non-single operations.

Such as:

public class Demo { private volatile int i = 0; public void increase() { i ++; // Non-atomic operation}}Copy the code

The “I ++” operation is actually a multi-step operation (the compiler supports this operation, but at compile time, it is divided into the following steps)

public class Demo { private volatile int i = 0; public void increase() { i ++; }}Copy the code

View bytecode through JavAP:

public class com.chris.demo.Demo {
  public com.chris.demo.Demo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_0
       6: putfield      #2                  // Field i:I
       9: return

  public void increase();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field i:I
       5: iconst_1
       6: iadd          //
       7: putfield      #2                  // Field i:I
      10: return
}
Copy the code

The above steps are divided into three steps: step 0 to step 2: read variable I from memory to register; Step 5 ~ 6: Add 1 to variable I; Step 7: Write the value of variable I back to memory;

Second, the role of volatile

2.1. Prevent reordering

Let’s start with a classic example: there are many ways to implement singletons, one of which is called DCL (Double Check Lock), which is implemented as follows:

If you write it like this, you’re making a mistake

public class Singleton {
    private static Singleton singleton;
    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}
Copy the code

The correct way to write this is:

public class Singleton { private volatile static Singleton singleton; / / here. Volatile private Singleton() {} public static Singleton getInstance() {if (Singleton == null) {synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; }}Copy the code

So let’s figure out why the first way is wrong, so let’s use the second way. The emphasis is on the order in which the JVM executes (reordering instructions that have no dependency requirements), so a bad DCL can be executed in one of two ways:

  1. The first order:
  • Allocate memory
  • Initialize memory
  • Memory assignment to the instance object
  1. The second order:
  • Allocate memory
  • Memory assignment to the instance object
  • Initialize memory

You might wonder why the second order is so obviously wrong.

As I said, both steps B and C depend on step A, but there is no dependency requirement for both, so the JVM may reorder! However, when we add the “volatile” keyword, we prevent the second order (that is, reordering), thus ensuring that memory assignments are made to instance objects.

2.2. Implement visibility

Visibility was explained in Synchronized: when multiple threads access a variable, one thread modifies its data, and the other threads immediately get the latest value, which can be considered a shared variable. The visibility problem is that each thread has its own cache: thread working memory (which is local memory for each thread, with an emphasis on “high speed”).

Let’s look at an example:

public class Demo { private int a = 1; private int b = 2; private void change() { a = 3; b = a; } private void print() { System.out.println("print => b = " + b + ", a = " + a); } public static void main(String[] args) { while (true) { final Demo test = new Demo(); new Thread(() -> { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } test.change(); }).start(); new Thread(() -> { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } test.print(); }).start(); }}}Copy the code

Guess what it prints (running for tens of seconds and then stopping)?

. Print => b = 2, a = 1... print => a = 3, b = 3 ... print => b = 3, a = 1 ...Copy the code

Theoretically there should be two outcomes: “b = 2, a = 1” or “b = 3, a = 3”, but in practice there are three. Why is this so? This is because, when the first thread changes to 3, the second thread is invisible. Both variables are visible to multiple threads if they are volatile.

2.3. Ensure atomicity

The atomicity problem has already been explained and is described in the JLS:

17.7 Non-atomic Treatment of double and long For the purposes of the Java Programming Language Memory Model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half. This can result in a situation where a thread sees the first 32 bits of a 64-bit value from one  write, and the second 32 bits from another write. Writes and reads of volatile long and double values are always atomic. Writes  to and reads of references are always atomic, regardless of whether they are implemented as 32-bit or 64-bit values. Some implementations may find it convenient to divide a single write action on a 64-bit long or double value into two write actions on adjacent 32-bit values. For efficiency's sake, this behavior is implementation-specific; an implementation of the Java Virtual Machine is free to perform writes to long and double values atomically or in two parts. Implementations of the Java Virtual Machine are encouraged to avoid splitting 64-bit values where possible. Programmers are encouraged to declare shared 64-bit values as volatile or synchronize their programs correctly to avoid possible complications.Copy the code

Double and long are non-atomic operations. For JMM purposes, non-volatile long or double has two steps: high 32 bits and low 32 bits. Therefore, it is recommended that volatile double and long variables be used for reading and writing.

Third, the principle of volatile

The deeper principles of volatile, which I will cover in a separate article, relate to the JMM (Java Memory Model).

3.1. Principle of Visibility

Since each thread has its own working memory, which is a local memory, each thread does not directly interact with the main memory, but does the corresponding operation through the working memory, so the data between each thread is not visible.

All that is visible to other threads about volatile variables is that:

  • Changing the value of this variable forces the corresponding variable in main memory to be flushed, and;
  • In the working memory of other threads, the value of this variable is invalid, and the value of this variable is re-read from the main memory.

3.2 Principle of order

This involves Java’s happen-before rule, defined in JSR 133 as follows:

Two actions can be ordered by a happens-before relationship.If one action happens before another, then the first is visible to and ordered before the second.

In a common sense, if A happens before B, then any operation done by A is visible to B. (It’s important to keep this in mind, because happen-before can easily be mistaken for before or after time).

Let’s take a look at what happens before rules are defined in JSR 133:

  1. Each action in a thread happens before every subsequent action in that thread.
  2. An unlock on a monitor happens before every subsequent lock on that monitor.
  3. A write to a volatile field happens before every subsequent read of that volatile.
  4. A call to start() on a thread happens before any actions in the started thread.
  5. All actions in a thread happen before any other thread successfully returns from a join() on that thread.
  6. If an action a happens before an action b, and b happens before an action c, then a happens before c.

Translation:

  1. Previous actions happen-before subsequent actions in the same thread. (That is, execute code sequentially within a single thread. However, it is legal for the compiler and processor to reorder without affecting the results of execution in a single-threaded environment. In other words, this rule does not guarantee compilation reordering and instruction reordering.
  2. The unlock action on the monitor happens -before its subsequent locking action. (Synchronized rule)
  3. Writes to volatile variables happen-before subsequent reads. (Volatile rules)
  4. The thread’s start() method happens -before all subsequent actions of the thread. (Thread start rule)
  5. All thread operations happen-before other threads call join on this thread and return successful operations.
  6. If a happens before B, b happens before C, then a happens before C (transitivity).

Here we focus on the third rule: rules that guarantee order for volatile variables. To implement volatile memory semantics, the JMM restricts both types of reordering to volatile variables. Here is a list of the rules for reordering volatile variables specified by the JMM:

Memory barriers

To implement the semantics of volatile visibility and happen-befor. The underlying JVM is done through something called a “memory barrier”. A memory barrier, also known as a memory barrier, is a set of processor instructions that implement sequential restrictions on memory operations.

  • LoadLoad barrier

Execution sequence: Load1 – >Loadload – >Load2

Ensure that Load2 and subsequent Load directives can access data loaded by Load1.

  • StoreStore barrier

Execution sequence: Store1 – >StoreStore – >Store2

Ensure that data from Store1 operations is visible to other processors before Store2 and subsequent Store instructions are executed.

  • LoadStore barrier

The execution sequence is Load1 – >LoadStore – >Store2

Ensure that data loaded by Load1 is accessible before Store2 and subsequent Store instructions are executed.

  • StoreLoad barrier

Execution sequence: Store1 – > StoreLoad – >Load2

Ensure that the data for Store1 is visible to other processors until Load2 and subsequent Load instructions are read.

Four,

In general, it is difficult to understand volatile. If not, don’t worry. It will take a while to fully understand, and we will see the use of volatile several times in future articles. Here is a brief overview of volatile basics and the original.

In general, volatile is an optimization for concurrent programming and can replace Synchronized in certain scenarios. However, volatile cannot completely replace Synchronized, and can only be used in special situations. In general, the following two conditions must be met to ensure thread-safety in a concurrent environment:

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