0

After reading a little bit about the java memory model and synchronization, a few questions came up:

Even if Thread 1 synchronizes the writes, then although the effect of the writes will be flushed to main memory, Thread 2 will still not see them because the read came from level 1 cache. So synchronizing writes only prevents collisions on writes. (Java thread-safe write-only hashmap)

Second, when a synchronized method exits, it automatically establishes a happens-before relationship with any subsequent invocation of a synchronized method for the same object. This guarantees that changes to the state of the object are visible to all threads. (https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html)

A third website (I can't find it again, sorry) said that every change to any object - it doesn't care where the reference comes from - will be flushed to memory when the method leaves the synchronized block and establishes a happens-before situation.

My questions are:

  1. What is really flushed back to memory by exiting the synchronized block? (As some websites also said that only the object whose lock has been aquired will be flushed back.)

  2. What does happens-before-relaitonship mean in this case? And what will be re-read from memory on entering the block, what not?

  3. How does a lock achieve this functionality (from https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/Lock.html):

    All Lock implementations must enforce the same memory synchronization semantics as provided by the built-in monitor lock, as described in section 17.4 of The Java™ Language Specification:

    A successful lock operation has the same memory synchronization effects as a successful Lock action. A successful unlock operation has the same memory synchronization effects as a successful Unlock action. Unsuccessful locking and unlocking operations, and reentrant locking/unlocking operations, do not require any memory synchronization effects.

If my assumtion that everything will be re-read and flushed is correct, this is achieved by using synchronized-block in the lock- and unlock-functions (which are mostly also necessary), right? And if it's wrong, how can this functionality be achieved?

Thank you in advance!

Quaffel
  • 1,463
  • 9
  • 19
  • If you want to know how the Java memory model works, you should strongly consider reading the [Java memory model](https://docs.oracle.com/javase/specs/jls/se9/html/jls-17.html#jls-17.4). – user2357112 Dec 31 '17 at 11:02
  • A synchronized block adds a [full membar](https://en.wikipedia.org/wiki/Memory_barrier) at the end of the block and forces the CPUs write cache to be fully flushed before further instructions are processed. – Boris the Spider Dec 31 '17 at 11:06
  • "*this is achieved by using synchronized-block in the lock- and unlock-functions (which are mostly also necessary)*" - you got it the wrong way around. [A `synchronized`-block uses the intrinsic object-lock](https://docs.oracle.com/javase/specs/jls/se7/html/jls-14.html#jls-14.19). – Turing85 Dec 31 '17 at 11:35
  • Wait, what? You mean the lock each object holds, right? I'm speaking of custom implementations of java.util.concurrent.locks.Lock, not the object-lock supervised by the monitor. (Or did I missunderstand something wrongly?) Or to be more specific: How can implementations of java.util.concurrent.locks.Lock ensure the same memory functionality that synchronize-blocks guarantee? – Quaffel Dec 31 '17 at 11:44
  • Look at the [source code](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/java/util/concurrent/locks/ReentrantReadWriteLock.java?av=f) @Quaffel ... – Boris the Spider Dec 31 '17 at 11:58
  • The object-intrinsic lock is some native implementation (I have never bothered to check if its implementation is open source, I treat it like a black box). By looking at [`ReentrantReadWriteLock`'s implementation](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/util/concurrent/locks/ReentrantReadWriteLock.java#ReentrantReadWriteLock), you will not find any `synchronized`-block. Disclaimer: I cannot claim to fully understand the implementation of `ReentrantReadWriteLock`. – Turing85 Dec 31 '17 at 11:59
  • That's correct @Turing85, they don't. – Boris the Spider Dec 31 '17 at 12:00
  • I've also looked for some special, native-implemented method-calls in both, the class itself and the used instances, but I didn't find anything. So how did they do that? Or does the quote mean something else? – Quaffel Dec 31 '17 at 12:45
  • The only suspicious function call I found is Unsafe#unpark as it would be logical if a parked thread re-reads from memory as a lot of data could have changed while it's been parked but this behaviour isn't documented anywhere. – Quaffel Dec 31 '17 at 13:05
  • @Quaffel all you need to implement a simple lock is volatile/atomic variables. If you look at AbstractQueuedSynchronizer(which is a core of all j.u.c. locks), you will see a lot of such things. – Andrey Cheboksarov Dec 31 '17 at 15:17
  • But using the volatile keyword doesn't guarantee the same memory effects as synchronized blocks to the whole thread, does it? It only guarantees data integrity to the volatile variable itself. – Quaffel Jan 01 '18 at 02:41
  • @Quaffel in fact, it does. Using volatile has almost the same guarantees as synchronized block. You can think of opening synchronized bracket as of 'volatile load', and closing synchronized bracket as of 'volatile store'. When you read volatile, you will also see all actions that happened before the corresponding write to that variable. – Andrey Cheboksarov Jan 01 '18 at 09:20

2 Answers2

4

The happens-before-relationship is the fundamental thing you have to understand, as the formal specification operates in terms of these. Terms like “flushing” are technical details that may help you understanding them, or misguide you in the worst case.

If a thread performs action A within a synchronized(object1) { … }, followed by a thread performing action B within a synchronized(object1) { … }, assuming that object1 refers to the same object, there is a happens-before-relationship between A and B and these actions are safe regarding accessing shared mutable data (assuming, no one else modifies this data).

But this is a directed relationship, i.e. B can safely access the data modified by A. But when seeing two synchronized(object1) { … } blocks, being sure that object1 is the same object, you still need to know whether A was executed before B or B was executed before A, to know the direction of the happens-before-relationship. For ordinary object oriented code, this usually works naturally, as each action will operate on whatever previous state of the object it finds.

Speaking of flushing, leaving a synchronized block causes flushing of all written data and entering a synchronized block causes rereading of all mutable data, but without the mutual exclusion guaranty of a synchronized on the same instance, there is no control over which happens before the other. Even worse, you can not use the shared data to detect the situation, as without blocking the other thread, it can still inconsistently modify the data you’re operating on.

Since synchronizing on different objects can’t establish a valid happens-before relationship, the JVM’s optimizer is not required to maintain the global flush effect. Most notably, today’s JVMs will remove synchronization, if Escape Analysis has proven that the object is never seen by other threads.

So you can use synchronizing on an object to guard access to data stored somewhere else, i.e not in that object, but it still requires consistent synchronizing on the same object instance for all access to the same shared data, which complicates the program logic, compared to simply synchronizing on the same object containing the guarded data.


volatile variables, like used by Locks internally, also have a global flush effect, if threads are reading and writing the same volatile variable, and use the value to form a correct program logic. This is trickier than with synchronized blocks, as there is no mutual exclusion of code execution, or well, you could see it as having a mutual exclusion limited to a single read, write, or cas operation.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • this reminds of the last talk Shipilev gave on the Java Memory Model; of course we should not even want to know these details as you said it yourself `misguide you in the worst case`, but I *know* there are other people like me that find this fascinating and it's simply impossible not to think about "flushes". – Eugene Jan 03 '18 at 21:27
  • Thank you for your detailed anser! I've only one question left according the memory model: "If x and y are actions of the same thread and x comes before y in program order, then hb(x, y)." But why is the compiler allowed to change the order of the code, wouldn't that hurt that rule? My assumption is: The compiler actually IS allowed to change the order of the code, but only if that doesn't hurt the _program order_ -> the swapped actions aren't allowed to have a relationship (or at least a loose one, like `foo = new Foo()`, so the value of foo might be assigned before the object is complete) – Quaffel Jan 03 '18 at 21:43
  • @Eugene I have the same problem: How should I be able to accept an answer about an implementation problem (and the memory model IS actually an implementation problem as it's only necessary due to the multicore-architecture of cpus) that isn't about the imlementation itself? Humans want to solve a problem completely and not just solving one problem by leaving something out/creating a new one. – Quaffel Jan 03 '18 at 21:47
  • To clearify my second question a bit: Yes, I know that I'm answering my question almost myself, but I want to know if my conclusion and my understanding of what the _program order_ is is right. – Quaffel Jan 03 '18 at 21:50
  • @Quaffel exactly - there are obviously rules on which optimizations can happen and which can't and btw this is what the barriers (volatile/locks) are for - to *prohibit* those potentially possible re-orderings that could be done by the compiler, CPU, etc – Eugene Jan 03 '18 at 21:53
  • @Quaffel: basically, you got it. The JVM’s optimizer may arrange the code in a different order, likewise the CPU may execute instructions in a different order, as long as the program’s behavior remains as-if being executed in program order, for a single thread. But two threads executing code in an optimized manner, may perceive each other out-of-order, unless they perform proper actions establishing *happens-before* relationships between them. More than often, there is no actual reordering, but caching and branch optimizations let it look like out of order when looking from the outside. – Holger Jan 04 '18 at 08:07
0

There is no flush per-se, it's just easier to think that way (easier to draw too); that's why there are lots of resources online that refer to flush to main memory (RAM assuming), but in reality it does not happen that often. What really happens is that a drain is performed of the load and/or store buffers to L1 cache (L2 in case of IBM) and it's up to the cache coherence protocol to sync data from there; or to put it differently caches are smart enough to talk to each other (via a BUS) and not fetch data from main memory all the time.

This is a complicated subject (disclaimer: even though I try to do a lot of reading on this, a lot of tests when I have time, I absolutely do not understand it in full glory), it's about potential compiler/cpu/etc re-orderings (program order is never respected), it's about flushes of the buffers, about memory barriers, release/acquire semantics... I don't think that your question is answerable without a phD report; that's why there are higher layers in the JLS called - "happens-before".

Understanding at least a small portion of the above, you would understand that your questions (at least first two), make very little sense.

What is really flushed back to memory by exiting the synchronized block

Probably nothing at all - caches "talk" to each other to sync data; I can only think of two other cases: when you first time read some data and when a thread dies - all written data will be flushed to main memory(but I'm not sure).

What does happens-before-relaitonship mean in this case? And what will be re-read from memory on entering the block, what not?

Really, the same sentence as above.

How does a lock achieve this functionality

Usually by introducing memory barriers; just like volatiles do.

Eugene
  • 117,005
  • 15
  • 201
  • 306
  • To be honest: I couldn't imagine understanding this extremely complex topic (at least partially) without your and Holger's help - it seems that the JLS requires the user to have a wide understanding of multithreading in general. (That might also be the reason why there are so many websites simplifying the JLS a lot more out there...) – Quaffel Jan 03 '18 at 22:24