3

The question has been posted before but no real example was provided that works. So Brian mentions that under certain conditions the AssertionError can occur in the following code:

public class Holder {
  private int n;
  public Holder(int n) { this.n = n; }

  public void assertSanity() {
    if (n!=n)
      throw new AssertionError("This statement is false");
  }
}

When holder is improperly published like this:

class someClass {
    public Holder holder;

    public void initialize() {
        holder = new Holder(42);
    }
}

I understand that this would occur when the reference to holder is made visible before the instance variable of the object holder is made visible to another thread. So I made the following example to provoke this behavior and thus the AssertionError with the following class:

public class Publish {

  public Holder holder;

  public void initialize() {
    holder = new Holder(42);
  }

  public static void main(String[] args) {
    Publish publish = new Publish();
    Thread t1 = new Thread(new Runnable() {
      public void run() {
        for(int i = 0; i < Integer.MAX_VALUE; i++) {
          publish.initialize();
        }
        System.out.println("initialize thread finished");
      }
    });

    Thread t2 = new Thread(new Runnable() {
      public void run() {
        int nullPointerHits = 0;
        int assertionErrors = 0;
        while(t1.isAlive()) {
          try {
            publish.holder.assertSanity();
          } catch(NullPointerException exc) {
            nullPointerHits++;
          } catch(AssertionError err) {
            assertionErrors ++;
          }
        }
        System.out.println("Nullpointerhits: " + nullPointerHits);
        System.out.println("Assertion errors: " + assertionErrors);
      }
    });

    t1.start();
    t2.start();
  }

}

No matter how many times I run the code, the AssertionError never occurs. So for me there are several options:

  • The jvm implementation (in my case Oracle's 1.8.0.20) enforces that the invariants set during construction of an object are visible to all threads.
  • The book is wrong, which I would doubt as the author is Brian Goetz ... nuf said
  • I'm doing something wrong in my code above

So the questions I have: - Did someone ever provoke this kind of AssertionError successfully? With what code then? - Why isn't my code provoking the AssertionError?

Community
  • 1
  • 1
Juru
  • 1,623
  • 17
  • 43
  • 1
    @SotiriosDelimanolis what the author said in the book is that the two threads may see Holder object in different states, so n may be initialized to 0 in the Object class in one thread. – grape_mao Oct 31 '14 at 21:26
  • Why are you delaying the start of `t2`? – John Bollinger Oct 31 '14 at 21:29
  • Historical reasons of that code. I used to loop until max integer for t2 too, instead of looking to t1.isAlive(). Because t2 would finish really fast with its execution it could finish before t1 didn't do any useful stuff. But ok now it is no longer needed. – Juru Oct 31 '14 at 21:48
  • I come back because I saw [this post](http://stackoverflow.com/questions/16159203/why-does-this-java-program-terminate-despite-that-apparently-it-shouldnt-and-d/16323196#16323196) – grape_mao Feb 13 '15 at 13:31
  • Good example @grape_mao (thanks), I'll have a go at that example as well. Wonder if I can adapt it to just one variable instead of two, that surely has an impact as well. – Juru Feb 16 '15 at 15:34

2 Answers2

3

Your program is not properly synchronized, as that term is defined by the Java Memory Model.

That does not, however, mean that any particular run will exhibit the assertion failure you are looking for, nor that you necessarily can expect ever to see that failure. It may be that your particular VM just happens to handle that particular program in a way that turns out never to expose that synchronization failure. Or it may turn out the although susceptible to failure, the likelihood is remote.

And no, your test does not provide any justification for writing code that fails to be properly synchronized in this particular way. You cannot generalize from these observations.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • Any idea of a program + VM (vendor + version) combination that would trigger the AssertionError? Unless I see it happening I won't believe this behavior, especially with a constructor involved. I would assume VM vendors would make sure that an object is delivered with invariants in place and visible to all threads after construction. Maybe I'll have to ask Brian himself at Devoxx Belgium in two weeks. – Juru Oct 31 '14 at 22:09
  • 2
    The particular example is intentionally simple, for demonstrative purposes. No real VM is likely to read `n` twice from main memory in the same execution of `Holder.assertSanity()`, which would be necessary to trigger the `AssertionError`, but it *could*. According to the JMM, assignment of all default values happens (conceptually) at the very beginning of the program, so you don't even need any instruction reordering for the two reads of `n` to see different values. You would be exceptionally ill-advised to rely on VMs to protect you from improper publication in general. – John Bollinger Nov 03 '14 at 20:10
  • @Juru, for what it's worth, [this](http://stackoverflow.com/questions/26740095/how-can-same-calculation-produce-different-results/26740581#26740581) looks like a *bona fide* example of improper publication. – John Bollinger Nov 04 '14 at 16:52
1

You are looking for a very rare condition. Even if the code reads an unintialized n, it may read the same default value on the next read so the race you are looking for requires an update right in between these two adjacent reads.

The problem is that every optimizer will coerce the two reads in your code into one, once it starts processing your code, so after that you will never get an AssertionError even if that single read evaluates to the default value.

Further, since the access to Publish.holder is unsynchronized, the optimizer is allowed to read its value exactly once and assume unchanged during all subsequent iterations. So an optimized second thread would always process the same object which will never turn back to the uninitialized state. Even worse, an optimistic optimizer may go as far as to assume that n is always 42 as you never initialize it to something else in this runtime and it will not consider the case that you want a race condition. So both loops may get optimized to no-ops.

In other words: if your code doesn’t fail on the first access, the likeliness of spotting the error in subsequent iterations dramatically drops down, possibly to zero. This is the opposite of your idea to let the code run inside a long loop hoping that you will eventually encounter the error.

The best chances for getting a data race are on the first, non-optimized, interpreted execution of your code. But keep in mind, the chance for that specific data race are still extremely low, even when running the entire test code in pure interpreted mode.

Holger
  • 285,553
  • 42
  • 434
  • 765