Abstract

In the previous article, WE talked about CPU caching causing visibility, thread switching causing atomicity, and compilation optimizations causing ordering problems. This article will address the visibility and order issues, leading to today’s main character: The Java memory model (often examined when interviewing concurrent).

What is the Java Memory model?

Now that you know that CPU caching causes visibility and compilation optimizations cause ordering problems, the easiest way is to simply disable CPU caching and compilation optimizations. But our performance is going to explode. We should disable it on demand. The Java Memory model has a very complex specification, but from a programmer’s point of view it specifies how the JVM provides ways to disable caching and compile optimizations on demand. This includes three keywords: volatile, synchronized, and final, and six happens-before rules.

The volatile keyword

Volatile means to disable CPU caching so that data variables are read and written directly from memory. If volatile Boolean v = false is used, then v must be read or written from memory, but this can be problematic up to Java version 1.5. In this code, if thread A executes write and thread B executes reader, and thread B determines this.v == true, what would x be?

public class VolatileExample {
    private int x = 0;
    private volatile boolean v = false;

    public void write(a) {
        this.x = Awesome!;
        this.v = true;
    }

    public void reader(a) {
        if (this.v == true) {
            // What is x here?}}}Copy the code

Prior to version 1.5, this value could be 666 or 0; Because x is not volatile, it must be 666 after version 1.5; Because of the happens-before rule.

What is the happens-before rule

The happens-before rule says that the result of a previous action is visible to the subsequent action. This rule may be confusing if you first encounter it, but reading it a few times will make sense.

1. Sequential rules of procedures

This rule states that in a thread, previous actions are happens-before any subsequent actions, in procedural order (meaning that the results of previous actions are visible to any subsequent actions). X = 666 happens-before this.v = true.

2.Volatile variable rules

This rule refers to writes to a Volatile variable, happens-before reads to that variable. If the variable is written by thread A, it will be visible to any thread. This means that CPU caching is disabled. If so, it will be the same as before version 1.5. So if you look at rule 3, it’s different.

3. The transitivity

This rule means that if A Happens-Before B, and B Happens-Before C. A Happens — Before C. That’s the rule of transitivity. Let’s take a look at that code again (let me copy it for you)

public class VolatileExample {
    private int x = 0;
    private volatile boolean v = false;

    public void write(a) {
        this.x = Awesome!;
        this.v = true;
    }

    public void reader(a) {
        if (this.v == true) {
            // Read variable x}}}Copy the code

X = 666 happens-before this.v = true, this.v = true happens-before X = 666 (this.v = true) {this.v = true} {this.v = true} If thread B executes the reader method and this.v == true, then x must be 666. This is where the semantics of volatile are enhanced in version 1.5. Before version 1.5, x might have been 0 because it was not volatile.

4. Rules of pipe lock

This rule states that the happens-before action to unlock a lock occurs Before the happens-before action to lock the lock. A pipe is a generic synchronization primitive. In Java, synchronized is the implementation of a pipe in Java. Locking in a pipe is implemented implicitly in Java. The following code automatically locks the synchronized code block before entering it and unlocks it after the block is executed. Here the lock and unlock are the compiler to help us achieve.

synchronized(this) { // Automatic lock here
   // x is a shared variable with an initial value of 0
   if (this.x < 12) {
      this.x = 12; }}// It will be unlocked automatically
Copy the code

In combination with the lock rule in the pipe, assuming that the initial value of X is 0 and the value will change to 12 after thread A executes the code block, then when thread A unlocks, thread B acquires the lock and enters the code block, it can see the execution result of thread A x = 12. This is the rule of locking in a pipe

5. Thread start() rule

This rule is about thread startup. It means that after main thread A starts child thread B, child thread B can see what the main thread does before it starts child thread B. HappensBefore: thread A calls thread B’s start method happens-before any operation in thread B. The reference code is as follows:

    int x = 0;
    public void start(a) {
        Thread thread = new Thread(() -> {
            System.out.println(this.x);
        });

        this.x = Awesome!;
        // The main thread starts the child thread
        thread.start();
    }
Copy the code

The variable x printed in the child thread is 666, so you can try it.

6. Thread join() rule

This rule is about thread waiting. It means that thread A waits for thread B to complete (by calling join() on thread B). When thread B completes, the main thread can see what the child does. If the join() method of child thread B is called in thread A and returns successfully, any action of child thread B is happens-before the subsequent action of the main thread calling the child thread Bjoin() method. The code is easy to understand. The sample code is as follows:

    int x = 0;
    public void start(a) {
        Thread thread = new Thread(() -> {
            this.x = Awesome!;
        });
        // The main thread starts the child thread
        thread.start();
        // The main thread calls the child thread's join method to wait
        thread.join();
        // the shared variable x == 666
    }
Copy the code

Ignored final

Prior to version 1.5, final fields were just like regular fields, except that their values could not be changed. In the Java memory model after 1.5, final type variable rearrangement was constrained. Now, as long as our constructor that provides the correct constructor does not escape, the last value of the final field initialized by the constructor must be visible to other threads. The code is as follows:

class FinalFieldExample {
  final int x;
  int y;
  static FinalFieldExample f;
  public FinalFieldExample(a) {
    x = 3;
    y = 4;
  }

  static void writer(a) {
    f = new FinalFieldExample();
  }

  static void reader(a) {
    if(f ! =null) {
      int i = f.x;
      intj = f.y; }}Copy the code

When a thread executes the reader() method and f! = null, then the final field modifier f.x must be 3, but y cannot be guaranteed to be 4 because it is not final. If this is before version 1.5, then f.x is also not guaranteed to be 3. So what is escape? Let’s modify the constructor:

  public FinalFieldExample(a) {
    x = 3;
    y = 4;
    // This is escape
    f = this;
  }
Copy the code

There is no guarantee that f.x == 3, even if the x variable is final. Why? Because an instruction rearrangement might occur in the constructor, the execution would look like this:

     // This is escape
    f = this;
    x = 3;
    y = 4;
Copy the code

So f of x is equal to 0. So if there is no escape in the constructor, the final modified field is fine. Refer to this document for detailed examples

conclusion

In this article, I could not understand the final constraint rearrangement in the last part of the article at the beginning. It was not until I searched online and read the information provided in the article that I slowly understood and read it repeatedly no less than ten times. Probably not very smart. The happens-before rule is at the heart of this article. Understand the happens-before rule.

Reference article: Geek Time: Java Concurrent Programming in Action 02

Personal blog: colablog.cn/

If my article helps you, you can follow my wechat official number and share the article with you as soon as possible