4

Here is the original code

//@author Brian Goetz and Tim Peierls
@ThreadSafe
public class SafePoint {
    @GuardedBy("this") private int x, y;

    private SafePoint(int[] a) {
        this(a[0], a[1]);
    }

    public SafePoint(SafePoint p) {
        this(p.get());
    }

    public SafePoint(int x, int y) {
        this.set(x, y);
    }

    public synchronized int[] get() {
        return new int[]{x, y};
    }

    public synchronized void set(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Here it is fine that the private int x,y are not final because the set method in the constructor makes for a happens before relationship when calling get because they use the same lock.

Now here is the modified version and a main method that I expected to throw an AssertionError after running it for a little bit because I removed the synchronized keyword in the set method. I made it private for the constructor to be the only one calling it in case someone was going to point out that it's not thread-safe because of it, which isn't the focus of my question.

Anyhow, I've waited quite a bit now, and no AssertionErrors were thrown. Now I am weary that this modified class is somehow thread-safe, even though from what I've learned, this is not because the x and y are not final. Can someone tell me why AssertionError is still never thrown?

public class SafePointProblem {
    static SafePoint sp = new SafePoint(1, 1);

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                final int finalI = new Random().nextInt(50);
                new Thread(() -> {
                    sp = new SafePoint(finalI, finalI);
                }).start();
            }
        }).start();
        while (true) {
            new Thread(() -> {
                sp.assertSanity();
                int[] xy = sp.get();
                if (xy[0] != xy[1]) {
                    throw new AssertionError("This statement is false 1.");
                }
            }).start();
        }
    }
}

class SafePoint {
    private int x, y;

    public SafePoint(int x, int y) {
        this.set(x, y);
    }

    public synchronized int[] get() {
        return new int[]{x, y};
    }

    // I removed the synchronized from here
    private void set(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public void assertSanity() {
        if (x != y) {
            throw new AssertionError("This statement is false2.");
        }
    }
}
St.Antario
  • 26,175
  • 41
  • 130
  • 318
katiex7
  • 863
  • 12
  • 23

2 Answers2

4

The fact that you have run this for a lot of time does not mean anything, it just means that at the moment you did not reproduce this; may be with a different jre or CPU this could break. Especially bad since the Murphy law will guarantee that this will happen somewhere in production and you will have a nightmare to debug.

A small example is not proof of good/correct code, especially true for concurrent code - which is extremely hard (I don't even dare to say to that I fully understand it). And you do understand that this is potentially bad since there is no happens-before.

Also making those variables final will mean that you can not set them via the setters, but only in constructor. So this means you can't have a setter, thus no one can alter the fields x and y once they are set, thus get is not supposed to be synchronized at all (I am talking about your SafePoint here)

Eugene
  • 117,005
  • 15
  • 201
  • 306
  • Actually I tried to investigate if it's even possible that `AssertionError` is thrown. Maybe HotSpot server compiler does optimizations that still will not allow the `Error` to be thrown. All I found was synchronization points came from `Random` and starting the threads... Not too useful... – St.Antario Jan 14 '18 at 17:49
  • Also upvoted and yes I am aware of the final field and its constraints. It was interesting to hear about how on a different jre or cpu this can break and thanks for confirming the concern about happens before – katiex7 Jan 14 '18 at 18:10
  • @katiex7 np, but I can't really teal how the accepted answer (I don't care which u accept btw) explain things. did `lock addl` or `synchronization entry` trigger that? – Eugene Jan 14 '18 at 18:39
  • Ah, I thought that the random.nextint was what was guaranteeing happens before from what I can telp – katiex7 Jan 14 '18 at 18:43
  • @katiex7 don't get me wrong, but any code that the `C1` compiler will generate is, well, sort of irrelevant in this case, the only thing to rely on is the specification - and as you already came to the same conclusion, your code is not correct under the JLS rules, which IMO is the only thing that counts here – Eugene Jan 14 '18 at 18:46
  • The time window for spotting inconsistencies here is ridiculously small, so it’s really unlikely to fail in a test. Of course, once the code is deployed, the customer’s machine is subject to Murphy's Law. Whereas on this particular developer machine, the optimized code may always transfer both `int` values in a single 64 bit transfer, never torn apart… – Holger Jan 16 '18 at 16:43
2

I'm not sure this question can be answered just with JMM so you may well get some sort of undefined behavior.

To investigate the question a little bit deeper we can try to decompile it. I ran this code compiled with HotSpot C2-compiler. Here is the fragment I could found (the whole compiled code is too long):

  0x00007f6b38516fbd: lock addl $0x0,(%rsp)     ;*synchronization entry
                                                ; - java.util.Random::<init>@-1 (line 105)
                                                ; - com.test.SafePointProblem$lambda::run@4 (line 19)

  0x00007f6b38516fc2: mov     0x10(%r10),%rax   ;*invokevirtual compareAndSwapLong
                                                ; - java.util.concurrent.atomic.AtomicLong::compareAndSet@9 (line 147)
                                                ; - java.util.Random::next@32 (line 204)
                                                ; - java.util.Random::nextInt@17 (line 390)
                                                ; - com.test.SafePointProblem$lambda

I'm not a HotSpot JIT-compiler expert, but from what I can see the compiled code contains synchronizations in all runnables of yours. Some of them came from Random::next (it uses CAS) which is atomic and reset CPU store buffers.

The exhausted answer to the question "Why?" can be quite complicated and definitely platform-dependent.

St.Antario
  • 26,175
  • 41
  • 130
  • 318
  • This is a really interesting answer. I might need to do a forloop with incrementing a counter instead of using random.nextint and see what happens Upvoted – katiex7 Jan 14 '18 at 18:08
  • @St.Antario I really can't tell what this code proves actually. `lock addl` is a `StoreLoad` barrier. what do you mean here? – Eugene Jan 14 '18 at 18:37
  • @St.Antario also a small portion of code is no proof towards specification (or the inverse), so even if a certain compiler *would* do something, others might not. The only thing to conform to is the specification. – Eugene Jan 14 '18 at 18:44
  • @Eugene Just took a look at the code more carefully. Have to agree, simple load-fence does not prove consistency here. Moreover I cannot see a way how to get `AssertionError` here. – St.Antario Jan 15 '18 at 07:23
  • @Eugene Since read/write references are atomic and new `finalI=new Random().nextInt(50)` _hb_ `new SafePoint(finalI, finalI)` when the reference is being published (even unsafely) if no `prefetchnta` instruction is used... I don't see any way to get that... – St.Antario Jan 15 '18 at 07:26
  • @St.Antario I haven't dug into the code too much either, neither have I run this to look. The point is - it is obviously not safe and it seems that you agree with this also. Now, how a *certain* compiler or VM does things is sort of irrelevant, I would not rely on it; the only thing I would is the specification – Eugene Jan 15 '18 at 08:47
  • @St.Antario btw I've explained here https://stackoverflow.com/a/48182585/1059372 how that would be possible at least theoretically, but there are many other posts like this that explain unsafe publishing an *potentially* a racy read – Eugene Jan 15 '18 at 08:49
  • @Eugene Yes, I agree that it is not safe and as I also mentioned the behavior is probably cannot be explained just using JMM. But HotSpot C2-compiler is commonly used and the actual behavior I think can be explained if we take into account the `compiler/CPU`. I think that was the OP asked about. I ran this code with `core i5` and `Ubuntu 14` btw... – St.Antario Jan 15 '18 at 08:58
  • @St.Antario if only you would understand russian... there is a great talk from Shipilev here https://www.youtube.com/watch?v=C6b_dFtujKo and he sort of talks what a JVM is allowed to do vs what a JMM is... – Eugene Jan 15 '18 at 09:03