Happens-before reasoning applied to some examples
suggest changeWe will present some examples to show how to apply happens-before reasoning to check that writes are visible to subsequent reads.
Single-threaded code
As you would expect, writes are always visible to subsequent reads in a single-threaded program.
public class SingleThreadExample {
public int a, b;
public int add() {
a = 1; // write(a)
b = 2; // write(b)
return a + b; // read(a) followed by read(b)
}
}
By Happens-Before Rule #1:
- The
write(a)
action happens-before thewrite(b)
action. - The
write(b)
action happens-before theread(a)
action. - The
read(a)
action happens-before theread(a)
action.
By Happens-Before Rule #4:
write(a)
happens-beforewrite(b)
ANDwrite(b)
happens-beforeread(a)
IMPLIESwrite(a)
happens-beforeread(a)
.write(b)
happens-beforeread(a)
ANDread(a)
happens-beforeread(b)
IMPLIESwrite(b)
happens-beforeread(b)
.
Summing up:
- The
write(a)
happens-beforeread(a)
relation means that thea + b
statement is guaranteed to see the correct value ofa
. - The
write(b)
happens-beforeread(b)
relation means that thea + b
statement is guaranteed to see the correct value ofb
.
Behavior of ‘volatile’ in an example with 2 threads
We will use the following example code to explore some implications of the Memory Model for `volatile.
public class VolatileExample {
private volatile int a;
private int b; // NOT volatile
public void update(int first, int second) {
b = first; // write(b)
a = second; // write-volatile(a)
}
public int observe() {
return a + b; // read-volatile(a) followed by read(b)
}
}
First, consider the following sequence of statements involving 2 threads:
- A single instance of
VolatileExample
is created; call itve
, ve.update(1, 2)
is called in one thread, andve.observe()
is called in another thread.
By Happens-Before Rule #1:
- The
write(a)
action happens-before thevolatile-write(a)
action. - The
volatile-read(a)
action happens-before theread(b)
action.
By Happens-Before Rule #2:
- The
volatile-write(a)
action in the first thread happens-before thevolatile-read(a)
action in the second thread.
By Happens-Before Rule #4:
- The
write(b)
action in the first thread happens-before theread(b)
action in the second thread.
In other words, for this particular sequence, we are guaranteed that the 2nd thread will see the update to the non-volatile variable b
made by the first thread. However, it is should also be clear that if the assignments in the update
method were the other way around, or the observe()
method read the variable b
before a
, then the happens-before chain would be broken. The chain would also be broken if volatile-read(a)
in the second thread was not subsequent to the volatile-write(a)
in the first thread.
When the chain is broken, there is no guarantee that observe()
will see the correct value of b
.
Volatile with three threads
Suppose we to add a third thread into the previous example:
- A single instance of
VolatileExample
is created; call itve
, - Two threads call
update
:
- `ve.update(1, 2)` is called in one thread,
- `ve.update(3, 4)` is called in the second thread,
ve.observe()
is subsequently called in a third thread.
To analyse this completely, we need to consider all of the possible interleavings of the statements in thread one and thread two. Instead, we will consider just two of them.
Scenario #1 - suppose that update(1, 2)
precedes update(3,4)
we get this sequence:
write(b, 1), write-volatile(a, 2) // first thread
write(b, 3), write-volatile(a, 4) // second thread
read-volatile(a), read(b) // third thread
In this case, it is easy to see that there is an unbroken happens-before chain from write(b, 3)
to read(b)
. Furthermore there is no intervening write to b
. So, for this scenario, the third thread is guaranteed to see b
as having value 3
.
Scenario #2 - suppose that update(1, 2)
and update(3,4)
overlap and the ations are interleaved as follows:
write(b, 3) // second thread
write(b, 1) // first thread
write-volatile(a, 2) // first thread
write-volatile(a, 4) // second thread
read-volatile(a), read(b) // third thread
Now, while there is a happens-before chain from write(b, 3)
to read(b)
, there is an intervening write(b, 1)
action performed by the other thread. This means we cannot be certain which value read(b)
will see.
(Aside: This demonstrates that we cannot rely on volatile
for ensuring visibility of non-volatile variables, except in very limited situations.)