The happens-before principle is a higher-level language description of visibility as specified in the Java Memory Model (JMM). Use this principle to solve the problem of visibility between two operations in a concurrent environment without getting bogged down in the arcane definitions of the Java memory model. The definition of visibility as defined in the Java memory model is not covered in this article, but books for interested readers are Understanding the Java Virtual Machine in Depth and the Art of Java Concurrent Programming.
1 definition of the happens-before principle
- Program Order Rule: In a thread, actions written earlier take place before those written later, in Order of control flow.
- Monitor Lock Rule: An UNLOCK operation occurs first when a subsequent Lock operation is performed on the same Lock.
- Volatile Variable Rule: Writes to a volatile Variable occur first after reads to that Variable.
- Thread Start Rule: The Thread object Start () method precedes every action of the Thread.
- Thread Termination Rule: All operations ina Thread occur first when the Thread terminates. We can detect whether the Thread has terminated by means of the thread.join () method and the return value of thread.isalive ().
- Interruption Rule: A call to the interrupt() method occurs when the interrupted Thread code detects that the Interruption has occurred. The interrupt() method detects that the Interruption has occurred.
- Finalizer Rule: An object’s initialization completion (constructor completion) occurs first at the start of its Finalize () method.
- Transitivity: If operation A precedes operation B and operation B precedes operation C, operation A precedes operation C.
The hardest part about the happens-before principle is how to understand the word “happens-before.” Our program order rules, for example, “in front of the first operation in writing after the operation of the”, if understood as “written in the previous operation than writing on the back of the operation to perform” seems to be no problem, write in front of the operation is on the program logic than written on the back of the operation to perform. An unlock operation takes place before a lock is performed on the same lock. The lock is unlocked before a lock is performed on the same lock.
The confusion arises because the “antecedent” is understood as an active rule requirement, and “antecedent” is actually the objective result of the program running. Correct interpretation of the way, for the same lock, if in the course of the program runs “a unlock operation first occurred in the same lock of a lock operation”, then “the impact of the unlock operation (modify sharing the value of a variable, sent a message, call the method) for the lock operation is visible”.
Follow this understanding and reinterpret the other rules in turn.
- Procedural order rule: In A thread, the effects of operation A are visible to operation B if operation A precedes operation B in control flow order.
- Pipe lock rule: If an UNLOCK operation is performed before a LOCK operation, the effect of the UNLOCK operation is visible to the lock operation.
- Rule for volatile Variables: For a volatile variable, if writes to that variable precede reads to that variable, the effect of writes to that variable is visible to reads to that variable.
- Thread start rule: For the same Thread object, the start() method of the Thread object occurs first for each action of the Thread, that is, the effect of the call to the start() method of the Thread is visible to each action of the Thread.
- Thread termination rules: For a Thread, all operations that occur in the Thread are detected before the Thread terminates, meaning that the effects of all operations in the Thread are visible to the calling Thread thread.join () or thread.isalive () methods.
- The thread interrupt rule: For the same thread, a call to the threadinterrupt () method occurs before the thread detects an interrupt event. That is, the effect of the threadinterrupt () method call is visible to the thread when the interrupt event is detected.
- Object finalization rule: For an object, the end of its constructor execution takes place at the start of Its Finalize () method first, that is to say, the impact of the end of an object’s constructor is visible to the start of its Finalize () method.
- Transitivity: If operation A precedes operation B and operation B precedes operation C, then operation A precedes operation C. That is, all effects of operation A are visible to operation C.
2 Sample code
2.1 Pipe lock rules
2.1.1 WithoutMonitorLockRule
The following code starts two threads, the updater and getter. The updater thread sets the stop variable to true, and the getter thread loops to terminate the loop if the stop variable is true. Running this program yields the following output.
updater set stop true.
The getter thread does not print getter stopped, indicating that the updater thread’s changes to the stop variable are not visible to the getter thread.
public class WithoutMonitorLockRule {
private static boolean stop = false;
public static void main(String[] args) {
Thread updater = new Thread(new Runnable() {
@Override
public void run(a) {
Utils.sleep(1000);
stop = true;
System.out.println("updater set stop true"); }},"updater");
Thread getter = new Thread(new Runnable() {
@Override
public void run(a) {
while (true) {
if (stop) {
System.out.println("getter stopped");
break; }}}},"getter"); getter.start(); updater.start(); }}Copy the code
2.1.2 MonitorLockRuleSynchronized
The following code defines the variable lockObject as a synchronous lock. Run the program and get the following output.
updater set stop true.
getter stopped.
This output indicates that changes made by the updater thread to the stop variable are visible to the getter thread. Combined with the happens-before principle, according to the program order rule, stop = true in the updater thread takes place Before the lockObject lock is released, and the lockObject lock is acquired Before if (stop) in the getter thread. Stop = true is visible to if (stop) because of transitivity.
public class MonitorLockRuleSynchronized {
private static boolean stop = false;
private static final Object lockObject = new Object();
public static void main(String[] args) {
Thread updater = new Thread(new Runnable() {
@Override
public void run(a) {
Utils.sleep(1000);
synchronized (lockObject) {
stop = true;
System.out.println("updater set stop true."); }}},"updater");
Thread getter = new Thread(new Runnable() {
@Override
public void run(a) {
while (true) {
synchronized (lockObject) {
if (stop) {
System.out.println("getter stopped.");
break; }}}}},"getter"); updater.start(); getter.start(); }}Copy the code
2.1.3 MonitorLockRuleReentrantLock
ReentrantLock can also have the same effects as the Synchronized keyword, as described in the Lock interface comments. All Lock interface implementations must have the same semantics for memory visibility as Synchronized.
Memory Synchronization All Lock implementations must enforce the same memory synchronization semantics as provided by The built-in monitor lock, as described in the Java Language Specification (17.4 Memory Model) : A successful lock operation has the same memory synchronization effects as a successful Lock action. A successful unlock operation has the same memory synchronization effects as a successful Unlock action.Copy the code
Writing this code with ReentrantLock as follows can achieve the same effects as Synchronized, as discussed in the following volatile variable rules.
public class MonitorLockRuleReentrantLock { private static boolean stop = false; private static ReentrantLock reentrantLock = new ReentrantLock(); public static void main(String[] args) { Thread updater = new Thread(new Runnable() { @Override public void run() { Utils.sleep(1000); reentrantLock.lock(); try { stop = true; System.out.println("updater set stop true."); } finally { reentrantLock.unlock(); } } }, "updater"); Thread getter = new Thread(new Runnable() { @Override public void run() { while (true) { reentrantLock.lock(); try { if (stop){ System.out.println("getter stopped."); break; } }finally { reentrantLock.unlock(); } } } }, "getter"); updater.start(); getter.start(); }}Copy the code
2.2 Volatile variable rules
2.2.1 WithoutVolatileRule
The following code starts two threads, the updater and getter. The updater thread sets the stop variable to true, and the getter thread loops to terminate the loop if the stop variable is true. Running this program yields the following output.
updater set stop true.
The getter thread does not print getter stopped, indicating that the updater thread’s changes to the stop variable are not visible to the getter thread.
public class WithoutVolatileRule {
private static boolean stop = false;
public static void main(String[] args) {
Thread updater = new Thread(new Runnable() {
@Override
public void run(a) {
Utils.sleep(1000);
stop = true;
System.out.println("updater set stop true"); }},"updater");
Thread getter = new Thread(new Runnable() {
@Override
public void run(a) {
while (true) {
if (stop) {
System.out.println("getter stopped");
break; }}}},"getter"); getter.start(); updater.start(); }}Copy the code
2.2.2 VolatileRule
Running the program after using the volatile keyword to modify the stop variable yields the following output.
updater set stop true.
getter stopped.
Note Changes made by the updater thread to stop are visible to the getter thread. Using the happens-before principle, according to the volatile variable rule, the updater thread writes the stop variable Before the getter thread reads the stop variable. So the updater thread setting the stop variable to true is visible to the getter thread reading the stop variable.
public class VolatileRule {
private static volatile boolean stop = false;
public static void main(String[] args) {
Thread updater = new Thread(new Runnable() {
@Override
public void run(a) {
Utils.sleep(1000);
stop = true;
System.out.println("updater set stop true."); }},"updater");
Thread getter = new Thread(new Runnable() {
@Override
public void run(a) {
while (true) {
if (stop) {
System.out.println("getter stopped.");
break; }}}},"getter"); updater.start(); getter.start(); }}Copy the code
2.2.3 VolatileRule1
Volatile variables also have a characteristic that they are not. The stop variable is not volatile, but the volatileObject variable is not volatile. Running this code yields the following output.
updater set stop true.
getter stopped.
public class VolatileRule1 {
private static boolean stop = false;
private static volatile Object volatileObject = new Object();
public static void main(String[] args) {
Thread updater = new Thread(new Runnable() {
@Override
public void run(a) {
Utils.sleep(1000);
stop = true;
volatileObject = new Object();
System.out.println("updater set stop true."); }},"updater");
Thread getter = new Thread(new Runnable() {
@Override
public void run(a) {
while (true) {
Object volatileObject = VolatileRule1.volatileObject;
if (stop) {
System.out.println("getter stopped.");
break; }}}},"getter"); updater.start(); getter.start(); }}Copy the code
The above results show that while the stop variable is not volatile, it is still visible between the updater thread and the getter thread, which is fascinating from Java syntax alone without a close reading of the happens-before principle.
The procedure order rules and transitivity of happens-before principle are carefully analyzed. The updater thread assigns the volatileObject variable after setting the stop variable to true, and the getter thread reads the volatileObject before reading the stop variable. Stop = true in the updater thread according to the sequence rule occurs when volatileObject = new Object(), Within the getter thread Object volatileObject = VolatileRule1. VolatileObject first occurred in the if (stop); Stop = true is visible to if (stop) because of transitivity.
After such analysis, it is found that this property is not magic, but only transitivity. For other happens-before principles, this property also exists, which can be verified by readers themselves. A typical example of using this feature in the Java JDK. Util. Concurrent. FutureTask, the following code excerpt from FutureTask, The comment after outcome reads non-volatile, protected by state reads/writes, and state is modified by volatile. This is why the execution results are visible when a task is submitted to the thread pool using the thread pool’s Submit method. When you submit a task to the thread pool using the execute method, the changes made to the task are not visible. Readers can verify for themselves.
private volatile int state;
/** The result to return or exception to throw from get() */
private Object outcome; // non-volatile, protected by state reads/writes
Copy the code
The already mentioned above to monitor lock rules ensure that the same related to volatile variables, in Java. Util. Concurrent. The locks. AbstractQueuedSynchronizer class of state property, This property is modified by the volatile keyword and will not be described again.
2.3 the Thread Start Rule
2.3.1 WithoutThreadStartRule
The following code starts the updater thread. The updater thread calls getter.start to start the getter thread, and the updater thread sets stop to true. Running the program yields the following output.
updater set stop true.
The getter thread does not print getter stopped, indicating that the updater thread’s changes to the stop variable are not visible to the getter thread.
public class WithoutThreadStartRule {
private static boolean stop = false;
public static void main(String[] args) {
Thread getter = new Thread(new Runnable() {
@Override
public void run(a) {
while (true) {if (WithoutThreadStartRule.stop) {
System.out.println("getter stopped.");
break; }}}},"getter");
Thread updater = new Thread(new Runnable() {
@Override
public void run(a) {
getter.start();
Utils.sleep(1000);
stop = true;
System.out.println("updater set stop true.");
while (true) {}}}); updater.start(); }Copy the code
2.3.2
Next switch getter.start() and stop = true in the updater thread, as shown in the code below. Running the program yields the following output.
updater set stop true.
getter stopped.
This output indicates that changes made by the updater thread to the stop variable are visible to the getter thread. Stop = true occurs first in get.start () in the updater thread according to the program order rule. Getter.start () for every action (including if (stop)) that takes place in the thread; According to the transitivity rule stop = true occurs first in if (stop), so stop = true is visible to if (stop).
public class ThreadStartRule {
private static boolean stop = false;
public static void main(String[] args) {
Thread getter = new Thread(new Runnable() {
@Override
public void run(a) {
while (true) {if (stop) {
System.out.println("getter stopped.");
break; }}}},"getter");
Thread updater = new Thread(new Runnable() {
@Override
public void run(a) {
stop = true;
System.out.println("updater set stop true.");
Utils.sleep(1000);
getter.start();
while (true) {}}}); updater.start(); }}Copy the code
2.4 Thread termination rules
Against 2.4.1 WithoutThreadTermination
The following code starts two threads, the updater thread and the getter. The updater thread sets the stop variable to true, and the getter loops to terminate the loop if the stop variable is true. Running the program yields the following output.
updater set stop true.
The getter thread does not print getter stopped, indicating that the updater thread’s changes to the stop variable are not visible to the getter thread.
public class WithoutThreadTerminationRule {
private static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread updater = new Thread(new Runnable() {
@Override
public void run(a) {
Utils.sleep(1000);
stop = true;
System.out.println("updater set stop true."); }}); Thread getter =new Thread(new Runnable() {
@Override
public void run(a) {
while (true) {if (stop) {
System.out.println("getter stopped.");
break; }}}},"getter"); updater.start(); getter.start(); }}Copy the code
2.4.2 ThreadTerminationRule
The getter thread calls updater.join() and waits for the upater thread to terminate before fetching the stop variable. Running this code yields the following output.
updater set stop true.
getter stopped.
This output indicates that changes made by the updater thread to the stop variable are visible to the getter thread. Combined with the happens-before principle, according to the thread termination rule, all operations (including stop = true) in the upater thread first occur when the getter thread calls upater. Join () and waits for the upater to end. Updater.join () occurs before if (stop) in the getter thread according to procedural order rules; Stop = true is visible to if (stop). Stop = true is visible to if (stop).
public class ThreadTerminationRule {
private static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread updater = new Thread(new Runnable() {
@Override
public void run(a) {
Utils.sleep(1000);
stop = true;
System.out.println("updater set stop true."); }}); Thread getter =new Thread(new Runnable() {
@Override
public void run(a) {
try {
updater.join();
} catch (InterruptedException e) {
}
while (true) {if (stop) {
System.out.println("getter stopped.");
break; }}}},"getter"); updater.start(); getter.start(); }}Copy the code
2.5 Thread interrupt rules
2.5.1 WithoutThreadInterruptRule
The following code starts two threads, the updater thread and the getter. The updater thread sets the stop variable to true, and the getter loops to terminate the loop if the stop variable is true. Running this code yields the following output.
updater set stop true.
The getter thread does not print getter stopped, indicating that the updater thread’s changes to the stop variable are not visible to the getter thread.
public class WithoutThreadInterruptRule {
private static boolean stop = false;
public static void main(String[] args) {
Thread getter = new Thread(new Runnable() {
@Override
public void run(a) {
while (true) {
if (stop) {
System.out.println("getter stopped.");
break; }}}},"getter");
Thread updater = new Thread(new Runnable() {
@Override
public void run(a) {
Utils.sleep(1000);
stop = true;
System.out.println("updater set stop true."); }},"updater"); updater.start(); getter.start(); }}Copy the code
2.5.2 ThreadInterruptRule
After setting stop to true, the updater thread calls getter.interrupt(), The getter Thread checks thread.currentThread ().isinterrupted () before fetching the stop variable. Run this code and get the following output.
updater set stop true.
getter stopped.
This output indicates that changes made by the updater thread to the stop variable are visible to the getter thread. According to the happens-before principle, stop = true occurs first in getter.interrupt() in the updater thread according to the program order rule. In the getter Thread, thread.currentThread ().isinterrupted () happens if (stop); According to the Thread interrupt rule, getter.interrupt() occurs first in thread.currentThread ().isinterrupted (); Stop = true is visible to if (stop). Stop = true is visible to if (stop).
public class ThreadInterruptRule {
private static boolean stop = false;
public static void main(String[] args) {
Thread getter = new Thread(new Runnable() {
@Override
public void run(a) {
while (true) {
if (Thread.currentThread().isInterrupted()) {
if (stop) {
System.out.println("getter stopped.");
break; }}}}},"getter");
Thread updater = new Thread(new Runnable() {
@Override
public void run(a) {
Utils.sleep(1000);
stop = true;
getter.interrupt();
System.out.println("updater set stop true."); }},"updater"); updater.start(); getter.start(); }}Copy the code
2.6 Object Termination Rules
2.6.1 FinalizerRule
In the constructor of the Test object, set the stop variable to true, and then assign the Test variable to null. The new Test object becomes garbage, and the object is allocated in an infinite loop to encourage garbage collection. Running the program yields the following output.
set stop true in constructor
stop true in finalize, threadName Finalizer
Stop = true occurs first in the Test constructor in the main thread according to the program order rule; The finalization of the constructor method of Test according to the object finalization rule occurs first at the beginning of finalize() method of Test. Then, according to transitivity, stop = true occurs at the beginning of Finalize () method first, so stop = true is visible to Finalize () method.
public class FinalizerRule {
private static boolean stop = false;
public static void main(String[] args) {
Test test = new Test();
//test is set to null and can be reclaimed
test = null;
while (true) {
// Promote garbage collection
byte[] bytes = new byte[1024 * 1024]; }}static class Test {
public Test(a) {
stop = true;
System.out.println("set stop true in constructor");
}
@Override
protected void finalize(a) throws Throwable {
if (stop) {
System.out.println("stop true in finalize, threadName " + Thread.currentThread().getName());
} else {
System.out.println("stop false in finalize, threadName "+ Thread.currentThread().getName()); }}}}Copy the code
Counterexamples of object finalization rules are particularly difficult to reproduce, and object finalization rules are hard to come by during programming, so they are not used here.
3 Further Interpretation
Many of the examples in Section 2 combine procedural ordering rules with transitivity to ensure visibility, so the first few rules are further interpreted as follows.
- Pipe lock rule: If an UNLOCK operation precedes a LOCK operation, the effect of the UNLOCK operation (including the operation before the UNLOCK operation) is visible to the lock operation (including the operation after the LOCK operation).
- Rule for volatile Variables: For a volatile variable, if writes to that variable precede reads to that variable, the effects of writes to that variable (including prior writes) are visible to reads to that variable (including subsequent reads).
- Thread start rule: For the same Thread object, the start() method of the Thread object occurs first for each action of the Thread, that is, the effect of the start() method call (including the operation before the start method) is visible to each action of the Thread.
- Thread termination rules: In the case of a Thread, all operations that occur in the Thread are preceded by the termination detection of that Thread, which means that the effects of all operations in the Thread are visible to the calling Thread’s thread.join () or thread.isalive () methods, including operations that occur after both methods are called.
- Thread interrupt rule: For the same thread, a call to the threadinterrupt () method occurs when the thread detects the occurrence of an interrupt event. That is, the effect of an interrupt() method call (including the operation before the interrupt call) is visible to the thread when it detects an interrupt event (including the operation after it detects an interrupt event).
- Object end rules: for the same object, its construction method performs the end of the first occurred in the finalize () method, that is an object of the end of the constructor (including the constructor) before the end of the effects, for it’s the finalize () method implemented (including after the start of operation) is visible.
For further interpretation, using the pipe locking rule as an example, the following code does not include stop = true and if (stop) in the Syncronized code block, but in the updater thread stop = true before the Sychronized code block, In the getter thread, if (stop) comes after the syncronized code block. Running the program yields the following output.
updater set stop true.
getter stopped.
Note Changes made by the updater thread to stop are visible to the getter thread.
public class MonitorLockRuleSynchronized1 {
private static boolean stop = false;
private static final Object lockObject = new Object();
public static void main(String[] args) {
Thread updater = new Thread(new Runnable() {
@Override
public void run(a) {
Utils.sleep(1000);
stop = true;
System.out.println("updater set stop true.");
synchronized (lockObject) {
}
}
}, "updater");
Thread getter = new Thread(new Runnable() {
@Override
public void run(a) {
while (true) {
synchronized (lockObject) {
}
if (stop) {
System.out.println("getter stopped.");
break; }}}},"getter"); updater.start(); getter.start(); }}Copy the code
4 summarizes
The happens-before principle is very important in Java concurrent programming, and it is difficult to write thread-safe and efficient multithreaded code without understanding happens-before. The happens-before principle is much simpler in its description of visibility than the Java memory model, but it is still tricky and hard to understand. In this paper, the happens-before principle is presented to readers in a common language and in combination with many examples of code, in the hope that it will be helpful for readers to understand the happens-before principle.
About the author
Wang Jianxin, head of Service governance in Zhuan Architecture Department, mainly responsible for service governance, RPC framework, distributed call tracking, monitoring system, etc. Love technology, love learning, welcome to contact and exchange.