Happens-before principle

We know that in the Java memory model, ordering can be achieved by volatile and synchronized, but if all ordering is done by these two keys alone, some operations become cumbersome, but we don’t feel that way when we write Java code. This is because the Java language has a happens-before principle. So what exactly is happens-before?

What is a happens-before

The concept of happens-before was first developed by Leslie Lamport in his seminal paper “Time, Clocks and the Ordering of Events in a Distributed System.” Leslie Lamport uses happens-before to define the partial ordering of events in a distributed system. In this paper, Leslie Lamport presents a distributed algorithm that extends this partial order relation into some kind of full order relation. Jsr-133 uses the concept of happens-before to specify the order of execution between two operations. Because these two operations can be within a thread, can also be between different threads. Thus, the JMM can provide programmers with a guarantee of memory visibility across threads through happens-before relationships. Jsr-133 :Java Memory Model and Thread Specification defines the happens-before relationship as follows: 1) If an action happens-before another, the execution result of the first action will be visible to the second action, and the execution order of the first action precedes the second action. Note: This is just a JMM guarantee to the programmer that 2) there is a happens-before relationship between the two operations, and it does not mean that the specific implementation of the Java platform must be executed in the order specified by the happens-before relationship. The reorder is not illegal (that is, the JMM allows it) if the result of the reorder is the same as the result of the happens-before relationship.

Example happens-before rule

There are some “natural” happens-before relationships in the Java memory model that already exist without the assistance of any synchronizer and can be used directly in coding (the definitions of the following eight rules refer to understanding the Java Virtual Machine in Detail, as well as examples from my own understanding of some of the rules) :

  1. Program Order Rule: In a thread, the actions written earlier take place before the actions that follow. To be precise, it should be the control flow sequence rather than the program code sequence, since structures such as branches and loops need to be considered. Examples of the following code: 1 happens-before 2,3 happens-before 4

    package com.zwx.happans.before;

    public class VolatileRule { private volatile boolean flag = false; private int a = 0;

    public void writer(){ a = 1; //1 flag = true; //2 } public void read(){ if(flag){//3 int i = a; / / 4}}Copy the code

    }

  2. Volatile Lock Rule: Writes to volatile variables must happen before subsequent reads to volatile variables. In the order rule example above, 2 happens-before 3 because flag is volatile

  3. Transitivity Rule: In the sequence Rule example above, according to the sequence Rule: 1 happens-before 2; According to the volatile variable rule: 2 happens-before 3; Therefore, according to the transitivity rule, it can be deduced that: 1 happens-before 3.

  4. Thread Start Rule: The Start () method of a Thread object that precedes every action of the Thread. In the following example, because 1 happens-before 2, and 2 happens-before all operations in thread 1, all values of x are visible to thread T1

    package com.zwx.happans.before;

    public class StartRule { static int x = 0;

    Public static void main(String[] args) {Thread t1 = new Thread(()->{system.out.println (x); //x=10 }); x = 10; //1 t1.start(); / / 2}Copy the code

    }

  5. Thread Termination Rule: All operations ina Thread happens-before can be used to detect the Termination of a Thread by means of thread.join (). In the following example, because 1 happens-before 2, and 2 happens-before 3, the value of x is changed in all t1, visible to the main thread

    package com.zwx.happans.before;

    import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;

    public class JoinRule { static int x = 0;

    public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(()->{ x=100; / / 1}); x = 10; t1.start(); t1.join(); //2 System.out.println(x); //3(x = 100, t1 = 0)}Copy the code

    }

  6. Monitor Lock Rule: An UNLOCK operation occurs first when a subsequent Lock operation is performed on the same Lock. It is important to note that it must be the same lock, and “behind” refers to the chronological order. If thread A first executes x++ once, then releases the lock, and then thread B enters the synchronized block, then thread B gets x 11:

    package com.zwx.happans.before;

    public class LockRule { int x = 0;

    Public void demo(){synchronized (this){// two threads access each other at the same time, and the values they modify are visible to each other x++; }}Copy the code

    }

  7. Interruption Rule: A call to the interrupt() method occurs when the interrupted Thread code detects that the Interruption has occurred. In this case, the interrupt() method detects that the Interruption has occurred

  8. Finalizer Rule: An object initialization is complete (constructor performs end) occurred in its first finalize () method If the relationship between the two operations were excluded, and cannot be derived from the above relationship, they are not sequential, virtual machine can be arbitrary to them to reorder, but no matter how random sort, as previously noted, The JMM follows a basic principle: the compiler and processor can be optimized as long as they don’t change the execution of the program (i.e., single-threaded programs and properly synchronized multithreaded programs). The reason for this is that the programmer does not care whether the two operations are actually reordered, but that the semantics of the program execution cannot be changed (that is, the execution result cannot be changed). Thus, the happens-before relationship is essentially the same thing as the as-if-serial semantics.

The as – if – serial semantics

The as-if-serial semantics mean that the execution result of a (single-threaded) program cannot be changed, no matter how much reordering is done (to improve parallelism by the compiler and processor). The compiler, runtime, and processor must comply with the AS-IF-Serial semantics. To comply with the as-if-serial semantics, the compiler and processor do not reorder operations that have data dependencies because such reordering changes the execution result. However, if there are no data dependencies between the operations, they can be reordered by the compiler and processor. To illustrate, take a look at the following code example:

package com.zwx.asifserial; public class AsIfSerialDemo { public static void main(String[] args) { int a = 10; //1 int b = 10; //2 int c = a+ b; / / 3}}Copy the code

In the example above, there is a data dependency between 1 and 3, and there is a data dependency between 2 and 3. Therefore, 3 cannot be reordered before 1 and 2 in the final sequence of instructions executed (the result of the program will be changed if 3 comes before 1 and 2). But there is no data dependency between 1 and 2, and the compiler and processor can reorder the execution order between 1 and 2.

conclusion

Happens-before relationships ensure that the execution results of properly synchronized multithreaded programs are not changed. The happens-before relationship creates a fantasy for programmers who write properly synchronized multithreaded programs: properly synchronized multithreaded programs are executed in the order specified by happens-before. The as-if-serial semantics guarantee that the execution result of a program in a single thread will not be changed. The as-if-serial semantics create a fantasy for programmers who write single-threaded programs: single-threaded programs are executed in sequence. Both the as-if-serial semantics and happens-before are intended to increase the parallelism of program execution as much as possible without changing the results of program execution.