Java Concurrency: The Happens-Before Guarantee
In this article, learn more about reorderings and multi-threaded codebases along with how Java helps you with its guarantees.
Join the DZone community and get the full member experience.
Join For FreeUsually, when we write code, we have the assumption that the code is executed in the same sequence as it was written. This is not the case, since for optimization purposes, a re-ordering of the statements happens either on compile time or runtime.
Regardless when a thread runs a program, the result should be as if all of the actions occurred in the order they appear in the program. The execution of the single thread program should follow as-if-serial semantics. Optimizations and re-orderings can be introduced as long as the result is guaranteed to be the same as the results of the program should the statements have been executed sequentially.
Let’s see an example.
This block:
var i = 0;
var j = 1;
j--;
Can be re-ordered to this block:
var j = 1;
j--;
var i = 0;
We can add an extra allocation depending on the results of the previous blocks.
var x = i+j;
Regardless of the re-orderings that occurred, the results should be as if each statement of the program was run sequentially.
From a single thread perspective, we are covered; however, when multiple threads operate on a block like this, there are various issues. The effects of a thread’s operations won’t be visible to the other thread in a predictable way.
Imagine the scenario where the execution of a code block by one thread is dependent on the results of the execution of another thread. This is the case of a happened-before relationship. We have two events, and the results should be the one as if one event happened before the other regardless of re-ordering.
Java has a happens-before guarantee.
Rules
We can check the documentation and see the rules that make the guarantee possible.
- An unlock on a monitor happens-before every subsequent lock on that monitor.
- A write to a
volatile
field happens-before every subsequent read of that field. - A call to
start()
on a thread happens-before any actions in the started thread. - All actions in a thread happen-before any other thread successfully returns from a
join()
on that thread. - The default initialization of any object happens-before any other actions (other than default-writes) of a program.
They are self-explanatory. Let’s check some of their code.
1. An Unlock on a Monitor Happens-Before Every Subsequent Lock on That Monitor.
Every object in Java has an intrinsic lock. When we use synchronized, we use an object’s lock.
Supposing we have a class and some methods, and we use the object’s lock:
public class HappensBeforeMonitor {
private int x = 0;
private int y = 0;
public void doXY() {
synchronized(this) {
x = 1;
y = 2;
}
}
}
Provided a thread calls doXY()
. The lock that the object has cannot be unlocked before a lock has been acquired. The synchronized method, as we have seen previously, wraps the code contained with a lock and unlock statement. Any re-ordering optimization should not change the order of the lock operation and the unlock operation.
2. A Write to a volatile Field Happens-Before Every Subsequent Read of That Field.
public class HappensBeforeVolatile {
private volatile int amount;
public void update(int newAmount) {
amount = newAmount;
}
public void printAmount() {
System.out.println(amount);
}
}
Assuming thread a
calls update and then thread b
calls to print the amount. The read will take place after the write. The write will write the value to the main memory. The result is thread b
to have the value set from thread a
.
3. A Call to start() on a Thread Happens-Before Any Actions in the Started Thread.
No re-ordering will affect the sequence between the actions on a thread and the action of a thread starting. All actions inside the thread will take place after the thread has started.
4. All Actions in a Thread Happen-Before Any Other Thread Successfully Returns From a join() on That Thread.
Thread b
calls join on thread a
. The operations inside thread a
will take place before the join. When thread b
's join call finishes, the changes in Thread a
will be visible to thread b
.
private int x = 0;
private int y = 1;
public void calculate() throws InterruptedException {
...
final Thread a = new Thread(() -> {
y = x*y;
});
Thread b = new Thread(() -> {
try {
a.join();
System.out.println(y);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
a.start();
b.start();
b.join();
...
}
5. The Default Initialization of Any Object Happens-Before Any Other Actions (Other Than Default-Writes) of a Program.
Take, for example, this plain class:
public class HappensBeforeConstructor {
private final int x;
private final int y;
public HappensBeforeConstructor(int a ,int b) {
x = a;
y = b;
}
}
If we think about it, the object instantiated inherits the Object.class
just like every object in Java. If the extension of Object
was not implicit, the class would be like this:
public class HappensBeforeConstructor extends Object {
private final int x;
private final int y;
public HappensBeforeConstructor(int a ,int b) {
super();
x = a;
y = b;
}
}
The super();
method instantiates the object. It’s the default initialization and no other operation in the constructor will be re-ordered and take place before it.
That’s all. In the next article, we will have a look at memory visibility.
Published at DZone with permission of Emmanouil Gkatziouras, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments