I am trying to understand the happens-before behavior of the volatile field when there's a mix of volatile and non-volatile fields.
Let's say there's 1 WriteThread and 5 ReadThreads, and they update/read the SharedObject.
ReadThreads call method waitToBeStopped()
from the beginning and WriteThread calls the method stop()
after 1 second.
public class SharedObject {
volatile boolean stopRequested = false;
int a = 0, b = 0, c = 0;
int d = 0, e = 0, f = 0;
// WriteThread calls this method
public void stop() {
a = 1;
b = 2;
c = 3;
stopRequested = true;
a = 4;
b = 5;
c = 6;
d = 7;
e = 8;
f = 9;
}
// ReadThread calls this method
public void waitToBeStopped() throws Exception {
while(!stopRequested) {
}
System.out.println("Stopped now.");
System.out.println(a + " " + b + " " + c + " " + d + " " + e + " " + f);
}
}
When this program ends, the output is something like this. Even when I try 100+ ReadThreads, the result is always the same.
Stopped now.
Stopped now.
Stopped now.
Stopped now.
Stopped now.
4 5 6 7 8 9
4 5 6 7 8 9
4 5 6 7 8 9
4 5 6 7 8 9
4 5 6 7 8 9
Q1. Can someone explain why this always returns 4,5,6,7,8,9 and not 1,2,3,0,0,0?
My understanding about happens-before relationship was something like this:
a=1,b=2,c=3
happens before WriteThread writes to stopRequested
stopRequested
happens before WriteThread writes to a=4,b=5,c=6,d=7,e=8,f=9
stopRequested
happens before ReadThread reads stopRequested
stopRequested
happens before ReadThread reads a,b,c,d,e,f
From these 4 statements, I cannot derive one like this...
a=4,b=5,c=6,d=7,e=8,f=9
happens before ReadThread reads a,b,c,d,e,f
Here's the other part of the code if it helps:
public class App {
public static void main(String[] args) throws Exception {
SharedObject sharedObject = new SharedObject();
for(int i =0 ; i < 5; i++) {
Runnable rThread = new ReadThread(sharedObject);
new Thread(rThread).start();
}
Runnable wThread = new WriteThread(sharedObject);
new Thread(wThread).start();
}
}
public class WriteThread implements Runnable {
private SharedObject sharedObject;
public WriteThread(SharedObject sharedObject) {
this.sharedObject = sharedObject;
}
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
sharedObject.stop();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public class ReadThread implements Runnable {
private SharedObject sharedObject;
public ReadThread(SharedObject sharedObject) {
this.sharedObject = sharedObject;
}
public void run() {
try {
sharedObject.waitToBeStopped();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
Your assumption that the writes after stopRequested = true;
are not garanteed to be visible to the readers is correct. The writer is not garanteed to do those writes to shared cache/memory where they'd become visible to the readers. It could just write them to its local cache and the readers wouldn't see the updated values.
The Java language makes garantees about visibility, e.g. when you use a volatile variable. But it doesn't garantee that changes on a non-volatile variable won't be visible to other threads. Such writes can still be visible like here in your case. The JVM implementation, the memory consistency model of the processor and other aspects influence visibility.
Note that the JLS, and the happens-before relationship, is a specification. JVM implementations and hardware often do more than the JLS specifies, which can lead to visibilities of writes that don't have to be visible by the JLS.