61 What Is the Happens Before Principle

61 What Is the happens-before Principle #

In this lesson, we will mainly explain what the happens-before relation is.

What is the happens-before relation? #

The happens-before relation is used to describe issues related to visibility: if the first operation happens before the second operation (or, it can be described as the first operation and the second operation satisfy the happens-before relation), then we can say that the first operation is definitely visible to the second operation, which means that when the second operation is executed, it is guaranteed to see the result of the first operation.

Example without happens-before relation #

Let’s start with an example that does not have a happens-before relation to further understand what it wants to express macroscopically. Let’s take a look at the following code:

public class Visibility {

    int x = 0;

    public void write() {

        x = 1;

    }

    public void read() {

        int y = x;

    }

}

The code is simple. There is an int x variable inside the class with an initial value of 0. The purpose of the write method is to rewrite the value of x to 1, while the purpose of the read method is to read the value of x.

If there are two threads, each executing the write and read methods, then because there is no mechanism for mutual cooperation between these two threads, the code inside the write and read methods does not have a happens-before relation, and the visibility of the variables cannot be guaranteed. Let’s use an example to illustrate this situation.

For example, suppose thread 1 has already executed the write method and modified the value of the shared variable x. Then thread 2 executes the read method to read the value of x. At this point, we cannot be sure whether thread 2 can now see the modification made by thread 1 to x. Thread 2 may see this modification, so the read value of x is 1, or it may not see this modification, so the read value of x is the initial value 0. Since there is uncertainty, the code inside the write and read methods does not have a happens-before relation. Conversely, if the first operation happens before the second operation, then the first operation is definitely visible to the second operation.

Now let’s take a look at the specific rules of the happens-before relation.

What are the rules of the happens-before relation? #

If there are operations x and y, we use hb(x, y) to represent x happens before y. (1) Single-thread rule: In a single thread, operations that occur earlier in the program code happen-before operations that occur later. In other words, if operations x and y are two operations within the same thread, and x appears before y in the code, then hb(x, y) holds. As shown in the following figure: img

This happens-before rule is very important because if the later statements within the same thread cannot see the execution results of the earlier statements, it will cause serious consequences, and the logical correctness of the program cannot be guaranteed.

Here is one important note. We have mentioned reordering before. Does this mean that happens-before rules conflict with reordering, and reordering is not allowed in order to satisfy happens-before relationships?

The answer is no. As long as the reordered result still meets the happens-before relationship, which means visibility can be guaranteed, the occurrence of reordering is not restricted. For example, within a single thread, if statement 1 appears before statement 2, according to the “Single-thread rule”, statement 1 happens-before statement 2. However, it does not mean that statement 1 must be executed before statement 2. For example, if statement 1 modifies the value of variable a, and the content of statement 2 is not related to variable a, statement 1 and statement 2 can still be reordered. Of course, if statement 1 modifies the value of variable a and statement 2 reads the value of variable a, then statement 1 will definitely be executed before statement 2.

(2) Lock rule (synchronized and Lock interface, etc.): If operation A is unlocking and operation B is locking the same lock, then hb(A, B) holds. As shown in the following figure: img

From the figure above, we can see that there are two threads, thread A and thread B. All operations before thread A unlocks the lock are visible to all operations after thread B locks the same lock. This is the rule of happens-before relationship for lock operations.

(3) Volatile variable rule: A write operation on a volatile variable happens-before a subsequent read operation on the same variable.

This means that if a variable is declared as volatile, every time it is modified, other threads reading this variable will always read the latest value. We have introduced the volatile keyword before and know that it guarantees visibility, which is specified by this rule.

(4) Thread start rule: The start method of a Thread object happens-before every action in the run method of that thread. As shown in the following figure:

img

In the example in the figure, the left area is thread A starting a new thread B, and the right area is thread B. When executing the statements in the run method of thread B, it will definitely see the results of all operations before thread A executes threadB.start().

(5) Thread join rule:

We know that join makes a thread wait for another thread. Suppose thread A starts a new thread B by calling threadB.start(), and then calls threadB.join(). Thread A will wait until the run method of thread B finishes (ignoring special circumstances such as interruption), and then the join method returns. After the join method returns, all subsequent operations in thread A can see the results of all operations executed in the run method of thread B, which means the operations in the run method of thread B happens-before the statements after thread A’s join. As shown in the following figure: img

(6) Interruption rule: A call to the interrupt method of a thread happens-before a check for the interrupted status of that thread.

This means that if a thread is interrupted by another thread, when checking the interruption (e.g. calling Thread.isInterrupted() method), the occurrence of this interruption can always be detected and there will be no inaccurate results.

(7) Concurrency utility class rule:

  • In a thread-safe concurrent container (e.g. HashTable), when getting a certain value, the result of previous put or other storage operations will always be visible. In other words, the storage operations on a thread-safe concurrent container happens-before read operations.
  • For Semaphore, it can release permits and acquire permits. The release operation happens-before the acquire operation. In other words, if there is a release operation before an acquire operation, it will definitely be visible during the acquire.
  • For Future, it has a get method to obtain the result of a task. So, when the get method of a Future gets the result, it can always see the results of all operations in the task. In other words, the operations in the Future task happens-before the get operation of the Future.
  • For thread pool, if you want to use it, you need to submit tasks (Runnable or Callable) into it. There is also a happens-before relationship rule for submitting tasks, which is that the submit operation happens-before the execution of the task.

Summary #

The above is an introduction to the happens-before relationship. In this lesson, we first introduced what the happens-before relationship is, and then gave an example where the happens-before relationship does not hold. Next, we focused on the various rules of the happens-before relationship. Most of these rules are already known or do not need to be separately memorized, but the rules you need to focus on and remember are: the happens-before rules for lock operations and the happens-before rules for volatile variables, because they are closely related to the use of synchronized and volatile. As for the happens-before rules for thread start, thread join, thread interruption, and concurrency utility classes, you don’t need to have a detailed understanding of them. In most cases, these rules are considered as known conditions and are used as defaults.