6

I'm wondering about result of unsafe decrementing/incrementing in java threads, so there is my program:

Main class:

public class Start {

    public static void main(String[] args) {

        int count = 10000000, pos = 0, neg = 0, zero = 0;

        for (int x=0; x<10000; x++) {

            Magic.counter = 0;

            Thread dec = new Thread(new Magic(false, count));
            Thread inc = new Thread(new Magic(true, count));

            dec.start();
            inc.start();

            try {
                inc.join();
                dec.join();
            } catch (InterruptedException e) {
                System.out.println("Error");
            }

            if (Magic.counter == 0)
                zero++;
            else if (Magic.counter > 0)
                pos++;
            else
                neg++;
        }

        System.out.println(Integer.toString(neg) + "\t\t\t" + Integer.toString(pos) + "\t\t\t" + Integer.toString(zero));
    }
}

Threads class:

public class Magic implements Runnable {

    public static int counter = 0;

    private boolean inc;
    private int countTo;

    public Magic(boolean inc, int countTo) {
        this.inc = inc;
        this.countTo = countTo;
    }

    @Override
    public void run() {

        for (int i=0;i<this.countTo;i++) {

            if (this.inc)
                Magic.counter++;
            else
                Magic.counter--;
        }

    }
}

I have run program few times, and always getting much more positive result then negative. I have also tried to change order of which threads starts but this changed nothing. Some results:

Number of results < 0 | Number of results > 0 | Number of results = 0

1103                8893                4
3159                6838                3
2639                7359                2
3240                6755                5
3264                6728                8
2883                7112                5
2973                7021                6
3123                6873                4
2882                7113                5
3098                6896                6
Nazin
  • 827
  • 3
  • 16
  • 31
  • You might want to make the Magic.counter variable volatile, otherwise operations may be reordered and/or its value may be cached per-thread in a register. – prunge Oct 20 '11 at 21:47

2 Answers2

6

I bet you will see the exact opposite behavior with the following change (that is, reverse the branches without changing anything else):

if (this.inc)
   Magic.counter--; // note change, and lie about `this.inc`
else
   Magic.counter++;

If true, what might this indicate about this indicate about the thread interactions?

Now, for fun, make Magic.counter volatile -- [how] do the results change?

What about removing volatile and surrounding the if/else with a lock? (A lock ensures a full memory-fence and establishes a critical region. It should always yield perfect results.)

Happy coding.


Things to consider:

  1. The code only looks at less than or greater to zero, not overall drift/variation: a +1 or -1 is all it takes to tip the scales. (It might be more useful to expand the data collected.)
  2. It takes ever so slightly longer to execute an "else" branch as a jump is required; normally this is a non-issue, but over 10 million cycles... one or two isn't much.
  3. Lack of volatile/memory-fence leaves for much lee-way in the visibility of the Magic.counter variable. (I believe a conforming JVM could actually yield far worse results...)
  4. The ++ and -- operators are inherently non-atomic.
  5. Thread interleaving is generally "non-deterministic"; less so if executed across multiple cores.
  • Changing order of Magic.counter--/++ "helps" a little, but its not exact opposite :) its like 5,5k negative, 4,5k positive – Nazin Oct 20 '11 at 22:03
  • @Nazin Interesting, thanks for reporting back :) One of the issues is that ++ and -- are non-atomic and the threads are interleaving in such a way that the read/write is "split" (wrt. the operation atomicity and value visibility between the threads). The `volatile` should help with the value visibility, but is insufficient to ensure a critical region (and thus atomic) `++/--` operation. –  Oct 20 '11 at 22:10
  • @Nazin Perhaps someone with more intricate details of the [particular] JVM implementation might know why `++` and `--` do not seem to be entirely symmetrical in this aspect. Best I can offer at this level is "non-deterministic" ;-) –  Oct 20 '11 at 22:15
  • `volatile` gave me strange results, with `Magic.counter--` first (in if) its about 4,2k negative and 5,8k positive... and it tooks about 30 times more to complete then before :). What about `lock`, I know it gives me perfect result, but that is not the problem :) – Nazin Oct 20 '11 at 23:14
  • @Nazin Those numbers from `volatile` seem to indicate that it's "more" of a non-atomic issue than a visibility issue. Of course, +1/-1 can easily throw off the results so... it might not be a very good indicator... –  Oct 21 '11 at 02:44
0

Generally speaking, this is due to the way the Java memory model works. You are accessing shared variables in two distinct threads without synchronization. Neither is the variable declared volatile, nor are you running atomar operations. The absence of coordination and atomar or volatile variables will lead to internal optimizations of the threaded code done by the JVM when executing. Also, non-volatile variables that have not crossed the memory barrier (i.e. synchronized) will lead to cached values per thread – and thus two conflicting thread-local copies inside their caches.

Given the absence of a sequential consistency model in Java, the complex runtime optimizations and the peculiarities of the used JVM and underlying system(single or multi-core, hyperthreading), it's impossible to predict the result deterministically – just because it is violating several multi-threading conventions for the Java language model. Running the exact same code on the same machine might still lead to similar results, but due to effects of thread scheduling, CPU usage of other OS processes etc. they will not likely be exactly the same.

Here are some resources about the JMM: http://www.cs.umd.edu/~pugh/java/memoryModel/

b_erb
  • 20,932
  • 8
  • 55
  • 64