So, follwoing some job interviews I wanted to write a small program to check that i++
really is non-atomic in java, and that one should, in practice,add some locking to protect it. Turns out you should, but this is not the question here.
So I wrote this program here just to check it.
The thing is, it hangs. It seems that the main thread is stuck on on t1.join()
line, even though both worker threads should finish because of the stop = true
from previous line.
I found that the hanging stops if :
boolean stop
as volatile
, causing the write to immediately
be seen by worker threads, ort
as volatile
... for this I have no idea what causes the un-hanging.Can someone explain what's going on? why do I see the hang and why does it stop in those three cases?
public class Test {
static /* volatile */ long t = 0;
static long[] counters = new long[2];
static /* volatile */ boolean stop = false;
static Object o = new Object();
public static void main(String[] args)
{
Thread t1 = createThread(0);
Thread t2 = createThread(1);
t1.start();
t2.start();
Thread.sleep(1000);
stop = true;
t1.join();
t2.join();
System.out.println("counter : " + t + " counters : " + counters[0] + ", " + counters[1] + " (sum : " + (counters[0] + counters[1]) + ")");
}
private static Thread createThread(final int i)
{
Thread thread = new Thread() {
public void run() {
while (!stop)
{
// synchronized (o) {
t++;
// }
// if (counters[i] % 1000000 == 0)
// {
// System.out.println(i + ")" + counters[i]);
// }
counters[i]++;
}
};
};
return thread;
}
}
Volatile keyword is used to modify the value of a variable by different threads. It is also used to make classes thread safe. It means that multiple threads can use a method and instance of the classes at the same time without any problem. The volatile keyword can be used either with primitive type or objects.
If you write volatile variable from multiple threads without using any synchronized constructs, you are bound to get data inconsistency errors.
Therefore, the volatile keyword does not provide thread safety when non-atomic operations or composite operations are performed on shared variables. Operations like increment and decrement are composite operations.
Those might be the possible reasons :
If you are interested in digging deeper into the topic then I would suggest to go over "Java Concurrency in Practice by Brian Goetz" book.
It seems that the main thread is stuck on on
t1.join()
line, even though both worker threads should finish because of thestop = true
from previous line.
In the absence of volatile
, locking, or other safe publication mechanism, the JVM has no obligation to ever make stop = true
visible to other threads. Specifically applied to your case, while your main thread sleeps for one second, the JIT compiler optimizes your while (!stop)
hot loop into the equivalent of
if (!stop) {
while (true) {
...
}
}
This particular optimization is known as "hoisting" of the read action out of the loop.
I found that the hanging stops if :
- I add some printing inside the worker threads (as in the comments), probably causing the worker threads to sometime give up CPU
No, it's because PrintStream::println
is a synchronized method. All known JVMs will emit a memory fence at the CPU level to ensure the semantics of an "acquire" action (in this case, lock acquisition), and this will force a reload of the stop
variable. This is not required by specification, just an implementation choice.
- If I mark the flag
boolean stop
as volatile, causing the write to immediately be seen by worker threads
The specification actually has no wall clock-time requirements on when a volatile write must become visible to other threads, but in practice it is understood that it must become visible "very soon". So this change is the correct way to ensure that the write to stop
is safely published to, and subsequently observed by, other threads reading it.
- If I mark the counter
t
as volatile... for this I have no idea what causes the un-hanging.
These are again the indirect effects of what the JVM does to ensure the semantics of a volatile
read, which is another kind of a "acquire" inter-thread action.
In summary, except for the change making stop
a volatile variable, your program switches from hanging forever to completing due to the accidental side-effects of the underlying JVM implementation, which for simplicity does some more flushing/invalidation of thread-local state than required by the specification.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With