54

In the following code:

class A {
    private int number;

    public void a() {
        number = 5;
    }

    public void b() {
        while(number == 0) {
            // ...
        }
    }
}

If method b is called and then a new thread is started which fires method a, then method b is not guaranteed to ever see the change of number and thus b may never terminate.

Of course we could make number volatile to resolve this. However for academic reasons let's assume that volatile is not an option:

The JSR-133 FAQs tells us:

After we exit a synchronized block, we release the monitor, which has the effect of flushing the cache to main memory, so that writes made by this thread can be visible to other threads. Before we can enter a synchronized block, we acquire the monitor, which has the effect of invalidating the local processor cache so that variables will be reloaded from main memory.

This sounds like I just need both a and b to enter and exit any synchronized-Block at all, no matter what monitor they use. More precisely it sounds like this...:

class A {
    private int number;

    public void a() {
        number = 5;
        synchronized(new Object()) {}
    }

    public void b() {
        while(number == 0) {
            // ...
            synchronized(new Object()) {}
        }
    }
}

...would eliminate the problem and will guarantee that b will see the change to a and thus will also eventually terminate.

However the FAQs also clearly state:

Another implication is that the following pattern, which some people use to force a memory barrier, doesn't work:

synchronized (new Object()) {}

This is actually a no-op, and your compiler can remove it entirely, because the compiler knows that no other thread will synchronize on the same monitor. You have to set up a happens-before relationship for one thread to see the results of another.

Now that is confusing. I thought that the synchronized-Statement will cause caches to flush. It surely can't flush a cache to main memory in way that the changes in the main memory can only be seen by threads which synchronize on the same monitor, especially since for volatile which basically does the same thing we don't even need a monitor, or am I mistaken there? So why is this a no-op and does not cause b to terminate by guarantee?

yankee
  • 38,872
  • 15
  • 103
  • 162
  • 3
    The reference isn't saved, so no other thread is capable of waiting on that reference. What are you trying to protect? – Elliott Frisch May 10 '16 at 15:10
  • 15
    This question perfectly illustrates the reason I urge people not to try to explain or understand a language's semantics in terms of implementation specific things that may or may not exist such as a fictional "local processor cache". – David Schwartz May 10 '16 at 17:43
  • 1
    @DavidSchwartz Exactly, and actually the phrase "cache flushing", especially when in the vicinity of the phrase "main memory" can be very much misleading about what actually happens. Cache coherency protocols often ensure memory consistency without actually reaching to the slow main memory. And if you abandon the JLS and the JMM and try to reason about the underlying architecture, you need to take into account things like the fact that some synchronization mechanisms don't ensure global memory consistency (demonstrated by IRIW). So, like yshavit said, sticking with the JLS is the way to go. – Dimitar Dimitrov May 11 '16 at 09:13

2 Answers2

50

The FAQ is not the authority on the matter; the JLS is. Section 17.4.4 specifies synchronizes-with relationships, which feed into happens-before relationships (17.4.5). The relevant bullet point is:

  • An unlock action on monitor m synchronizes-with all subsequent lock actions on m (where "subsequent" is defined according to the synchronization order).

Since m here is the reference to the new Object(), and it's never stored or published to any other thread, we can be sure that no other thread will acquire a lock on m after the lock in this block is released. Furthermore, since m is a new object, we can be sure that there is no action that previously unlocked on it. Therefore, we can be sure that no action formally synchronizes-with this action.

Technically, you don't even need to do a full cache flush to be up to the JLS spec; it's more than the JLS requires. A typical implementation does that, because it's the easiest thing the hardware lets you do, but it's going "above and beyond" so to speak. In cases where escape analysis tells an optimizing compiler that we need even less, the compiler can perform less. In your example, escape analysis can could tell the compiler that the action has no effect (due to the reasoning above) and can be optimized out entirely.

yshavit
  • 42,327
  • 7
  • 87
  • 124
  • 5
    So that means that theoretically speaking we don't need to flush the cache at all. But we must make sure that another thread synchronizing on the same monitor can see everything that happened up to the point where the previous thread left the synchronized block and the easiest way to guarantee this is to flush the read cache on enter and the write cache on exit. Correct? – yankee May 10 '16 at 15:31
  • 2
    @yankee What the FAQ is saying, clumsily and foolishly IMO, is that if there were such a cache, and flushing that cache was what was needed to make monitors work as they must work, then that cache would be flushed. Why that's a smart thing to say, I don't know. It leads, IMO, to much more misunderstanding than understanding. – David Schwartz May 10 '16 at 17:45
  • 11
    Besides Escape Analysis, which is capable of identifying purely local objects, there are other code transformations potentially destroying the global cache flush effect of an invalid synchronization. For example, it is legal to *expand* the protected region of `synchronized` statements to merge multiple subsequent synchronizations or multiple iterations of a loop into one, reducing the number of acquire and release operations. Then, since no other thread can acquire the lock in-between, no *happens-before* relationship can exist and no flushing is required while still holding the lock. – Holger May 10 '16 at 17:57
  • @Holger to questions like this there are 5-10 people here that I immediately search for when trying to understand. :) You are one of them. Thx for your input – Eugene May 17 '16 at 20:04
21

the following pattern, which some people use to force a memory barrier, doesn't work:

It's not guaranteed to be a no-op, but the spec permits it to be a no-op. The spec only requires synchronization to establish a happens-before relationship between two threads when the two threads synchronize on the same object, but it actually would be easier to implement a JVM where the identity of the object did not matter.

I thought that the synchronized-Statement will cause caches to flush

There is no "cache" in the Java Language Specification. That's a concept that only exists in the details of some (well, O.K., virtually all) hardware platforms and JVM implementations.

Solomon Slow
  • 25,130
  • 5
  • 37
  • 57
  • 1
    So that basically means that the specification allows synchronizations to be postponed until another synchronize action on the same monitor happens? – yankee May 10 '16 at 15:20
  • 1
    > There is no "cache" in the Java Language Specification" Actually the JLS says things like "the compiler does not have to flush writes cached in registers" in [chapter 17](https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html). Sounds to me like they are using registers as cache, so there is a cache in the JLS. – yankee May 10 '16 at 15:21
  • 1
    @Yankee That's an explanation in a non memory model related part of the JLS. If you read the actual definition of the JMM you won't find any reference to a cache or register (except possibly in non normative explanatory parts). – Voo May 10 '16 at 15:28
  • 2
    @yankee That assumes that there are registers and that things are cached in register, which is not required. Of course, the compiler is never required to do anything but comply with the standard. If the compiler uses registers, it is only required to flush them when that is necessary to comply with the standard's actual requirements. The compiler doesn't have to do anything unless it's required to comply with the standard. – David Schwartz May 10 '16 at 17:46
  • 6
    @yankee The compiler does not have to throw a fish at the ceiling. That doesn't mean there are fish or ceilings in Java. – user253751 May 11 '16 at 06:12