views:

422

answers:

5

JSR-133 FAQ says:

But there is more to synchronization than mutual exclusion. Synchronization ensures that memory writes by a thread before or during a synchronized block are made visible in a predictable manner to other threads which synchronize on the same monitor. 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. We will then be able to see all of the writes made visible by the previous release.

I also remember reading that on modern Sun VMs uncontended synchronizations are cheap. I am a little confused by this claim. Consider code like:

class Foo {
    int x = 1;
    int y = 1;
    ..
    synchronized (aLock) {
        x = x + 1;
    }
}

Updates to x need the synchronization, but does the acquisition of the lock clear the value of y also from the cache? I can't imagine that to be the case, because if it were true, techniques like lock striping might not help. Alternatively can the JVM reliably analyze the code to ensure that y is not modified in another synchronized block using the same lock and hence not dump the value of y in cache when entering the synchronized block?

A: 

Since y is out of scope of the synchronized method there are no guarantees that changes to it are visible in other threads. If you want to guarantee that changes to y are seen the same across all threads, then all threads must use synchronization when reading/writing y.

If some threads change y in a synchronized manner and others don't then you'll get unexpected behavior. All mutable state shared between threads must be synchronized for there to be any guarantees on seeing changes between threads. All access on all threads to shared mutable state (variables) must be synchronized.

And yes, the JVM guarantees that while the lock is held no other thread can enter a region of code protected by the same lock.

Jeremy Raymond
ne0sonic, I think you have misunderstood the question. Please correct me if I am wrong.
binil
There are _no_ guarantees of what happens to variables modified by multiple threads outside of a locked context. So if y is modified by multiple threads who knows what the value will be, maybe the value last modified by the current thread, maybe some value from another thread. If it's thread local (modified only by a single thread ever) then you'll see the most up to date value.
Jeremy Raymond
In the original example y isn't in the synchronized block so when the code is executed the JVM won't do anything special to ensure y is up to date if modified by another thread. It may or may not see the changes made by other threads.
Jeremy Raymond
Not true at all, ne0sonic, at least in the Java 5 memory model. There are guarantees around the values of y which will be seen -- any thread entering a synchronized block sees the 'current' values of _all_ variables visible to the last thread the leave the sync block, at least 'as at' (and potentially after) the time of releasing the lock. That is, the happens-before of the release and the acquire on the lock ensure visibility of all state visible to the releasor thread, not just those changes which happened during the synchronized block (or just to those variables affected within the block)
Cowan
Do you have a reference in the spec where it says this? You're saying that entering a synchronized block you're guaranteed to see the most recent values of items not protected by the lock? While in the synchronized region the value of y could be changed on another thread and that may not be visible to the thread holding the lock.
Jeremy Raymond
ne0sonic is right here. There are absolutely no guarantees that y will be up to date after exiting that synchronized block. The happens before edge ends at the end of that block. Because of that the compiler can re order the y = y + 1, and then you lose any type of sequential consistency. The memory barriers employed within a synchronized block, reentrant lock or volatile variable do not take affect unless they are within that block.
John V.
A bit of a technicality. A volatile variable itself would employ the memory barrier. Any other object accessible by multiple threads that is not volatile needs to be in a synchronized block to be seen correctly by all threads.
John V.
OK sorry ne0sonic, I'm not sure we're discussing exactly the same thing and I may have misinterpreted your original comment. No, on entering a synchronized block you're not guaranteed to see the latest value of (say) y; but you are guaranteed to see _at least_ the value of y that was visible to a previous thread at the time it _released_ the lock. For example if y starts as 1, thread B does "y = 2; synchronized (aLock) { x = 9 }; y = 3;" then when thread B takes a lock on aLock, it is guaranteed to see _at least_ the value of 2 for y.
Cowan
Sorry, make that first reference 'thread A', not 'thread B'. Sigh.
Cowan
Ah, OK. You may be right. What fun the complexities of the Java Memory Model.
Jeremy Raymond
For my explanation to a previous poster of how you can work that behaviour out from the JLS, ne0sonic, see http://stackoverflow.com/questions/1351168/volatile-semantic-with-respect-to-other-fields/1356376#1356376 -- that's about volatile writes then reads but only step '2' in my explanation is different, the same happens with synchronized (where write and read are replaced with releasing and taking a particular object's lock respectively)
Cowan
@Cowan, so you are suggesting that by getting the monitor, the thread has to dump its cached copy of y? Or can the compiler notice that y is not read anywhere in the block for which the monitor was acquired, and hence not bother with the refresh?
binil
+1  A: 

Updates to x need the synchronization, but does the acquisition of the lock clear the value of y also from the cache? I can't imagine that to be the case, because if it were true, techniques like lock striping might not help.

I'm not sure, but I think the answer may be "yes". Consider this:

class Foo {
    int x = 1;
    int y = 1;
    ..
    void bar() {
        synchronized (aLock) {
            x = x + 1;
        }
        y = y + 1;
    }
}

Now this code is unsafe, depending on what happens im the rest of the program. However, I think that the memory model means that the value of y seen by bar should not be older than the "real" value at the time of acquisition of the lock. That would imply the cache must be invalidated for y as well as x.

Also can the JVM reliably analyze the code to ensure that y is not modified in another synchronized block using the same lock?

If the lock is this, this analysis looks like it would be feasible as a global optimization once all classes have been preloaded. (I'm not saying that it would be easy, or worthwhile ...)

In more general cases, the problem of proving that a given lock is only ever used in connection with a given "owning" instance is probably intractable.

Stephen C
+1  A: 

we are java developers, we only know virtual machines, not real machines!

let me theorize what is happening - but I must say I don't know what I'm talking about.

say thread A is running on CPU A with cache A, thread B is running on CPU B with cache B,

  1. thread A reads y; CPU A fetches y from main memory, and saved the value in cache A.

  2. thread B assigns new value to 'y'. VM doesn't have to update the main memory at this point; as far as thread B is concerned, it can be reading/writing on a local image of 'y'; maybe the 'y' is nothing but a cpu register.

  3. thread B exits a sync block and releases a monitor. (when and where it entered the block doesn't matter). thread B has updated quite some variables till this point, including 'y'. All those updates must be written to main memory now.

  4. CPU B writes the new y value to place 'y' in main memory. (I imagine that) almost INSTANTLY, information 'main y is updated' is wired to cache A, and cache A invalidate its own copy of y. That must have happened really FAST on the hardware.

  5. thread A acquires a monitor and enters a sync block - at this point it doesn't have to do anything regarding cache A. 'y' has already gone from cache A. when thread A reads y again, it's fresh from main memory with the new value assigned by B.

consider another variable z, which was also cached by A in step(1), but it's not updated by thread B in step(2). it can survive in cache A all the way to step(5). access to 'z' is not slowed down because of synchronization.

if the above statements make sense, then indeed the cost isn't very high.


addition to step(5): thread A may have its own cache which is even faster than cache A - it can use a register for variable 'y' for example. that will not be invalidated by step(4), therefore in step(5), thread A must erase its own cache upon sync entering. that's not a huge penalty though.

irreputable
Just a note from my understanding... in (3) you say "All those updates must be written to main memory now." I think this is not true from my reading of the issues with double-checked locking. The start of a synchronization block guarantees that you see the latest data but there is no explicit flush at the end of a block. Between that and the fact that statements can be reordered, you cannot expect a consistent view of data unless you are also in a synchronized block. The volatile keyword changes some of these semantics but mostly guarantees ordering and flushing.
PSpeed
updates must be flushed to main memory at end of a sync block, so the next sync block started by another thread can see those updates. reordering happens, but it can't violate "happens-before" relations.
irreputable
Not technically correct, irreuptable... the data doesn't need to be flushed at the end. A 'flush' could perfectly legally happen right before the next thread takes the lock, it doesn't need to occur right after (or before) the updating thread releases it to meet the memory model.
Cowan
+4  A: 

JSR-133 goes too far in its explanation. The Java memory model is formally defined in terms of things like visibility, atomicity, happens-before relationships and so on, which explains exactly what threads must see what, what actions must occur before other actions and so on. Behavior which isn't formally defined could be random, or well-defined in practice on some hardware and JVM implementation - but of course you should never rely on this, as it might change in the future, and you could never really be sure that it was well-defined in the first place unless you wrote the JVM and were well-aware of the hardware semantics.

So the text that you quoted is not formally describing what Java guarantees, but rather is describing how some hypothetical architecture which had very weak memory ordering and visibility guarantees could satisfy the Java memory model requirements using cache flushing. Any actual discussion of cache flushing, main memory and so on is clearly not generally applicable to Java as these concepts don't exist in the abstract language and memory model spec.

In practice, the guarantees offered by the memory model are much weaker than a full flush - having every atomic, concurrency-related or lock operation flush the entire cache would be prohibitively expensive - and this is almost never done in practice. Rather, special atomic CPU operations are used, sometimes in combination with barrier operations which help ensure memory visibility and ordering. So the apparent inconsistency between cheap uncontended synchronization and "fully flushing the cache" is resolved by noting that the first is true and the second is not - no full flush is required by the Java memory model (and no flush occurs in practice).

BeeOnRope
A: 

synchronize guarantees, that only one thread can enter a block of code. But it doesn't guarantee, that variables modifications done within synchronized section will be visible to other threads. Only the threads that enters the synchronized block is guaranteed to see the changes. Memory effects of synchronization in Java could be compared with the problem of Double-Checked Locking with respect to c++ and Java Double-Checked Locking is widely cited and used as an efficient method for implementing lazy initialization in a multi-threaded environment. Unfortunately, it will not work reliably in a platform independent way when implemented in Java, without additional synchronization. When implemented in other languages, such as C++, it depends on the memory model of the processor, the re-orderings performed by the compiler and the interaction between the compiler and the synchronization library. Since none of these are specified in a language such as C++, little can be said about the situations in which it will work. Explicit memory barriers can be used to make it work in C++, but these barriers are not available in Java.

Sudhakar Kalmari