8
votes

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 :

  • I add some printing inside the worker threads (as in the comments), probably causing the worker threads to sometime give up CPU or
  • If I mark the flag boolean stop as volatile, causing the write to immediately be seen by worker threads, or
  • If I mark the counter tas 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;
    }
}
4

4 Answers

8
votes

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.

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.

2
votes

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.

0
votes

Marking a variable as volatile is a hint for the JVM, to flush/sync the related segments of cache between threads/cores when that variable is updated. Marking stop as volatile then has better behaviour (but not perfect, you may have some extra executions on your threads before they see the update).

Marking t as volatile puzzles me as to why it works, it may be that because this is such a small program, t and stop are in the same row in the cache, so when one gets flushed/synced the other does too.

System.out.println is thread safe, so there is some synchronization going on internally. Again, this may be causing some parts of the cache to be synced between the threads.

If anyone can add to this, please do, I also would really like to hear a more detailed answer on this.

0
votes

It does, actually, what it said to -- provides consistent access to the field between multiple threads, and you can see it.

Without volatile keyword, multithread access to the field is not guaranteed to be consistent, compilers can introduce some optimisations, like caching it in the CPU register, or not writing out from CPU core local cache to external memory or shared cache.


For part with non-volatile stop and volatile t

According to JSR-133 Java Memory Model specification, all writes (to any other field) before volatile field update are made visible, they are happened-before actions.

When you set stop flag after incrementing t, it will be not visible by subsequent read in the loop, but next increment (volatile-write) will make it visible.


See also

Java Language Specification: 8.3.1.4. volatile Fields

An article about Java Memory Model, from the author of Java theory and practice