Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java Memory Model: Mixing unsynchronized and synchronized

Suppose there is a class like this:

public void MyClass {
    private boolean someoneTouchedMeWhenIWasWorking;

    public void process() {
        someoneTouchedMeWhenIWasWorking = false;
        doStuff();
        synchronized(this) {
            if (someoneTouchedMeWhenIWasWorking) {
                System.out.println("Hey!");
            }
        }
    }

    synchronized public void touch() {
        someoneTouchedMeWhenIWasWorking = true;
    }
}

One thread calls process, another calls touch. Note how process clears the flag without synchronization when it starts.

With Java memory model, is it ever possible that the thread running process will see the effect of its local, unsynchronized write, even though touch happened later?

That is, if the threads execute in this order:

T1: someoneTouchedMeWhenIWasWorking = false; // Unsynchronized
T2: someoneTouchedMeWhenIWasWorking = true; // Synchronized
T1: if (someoneTouchedMeWhenIWasWorking) // Synchronized

... is it ever possible that T1 would see the value from its local cache? Can it flush what it has to memory first (overriding whatever T2 has written), and reload that value from memory?

Is it necessary to synchronize the first write or make the variable volatile?

I would really appreciate it if the answers were backed by documentation or some respectable sources.

like image 978
Konrad Garus Avatar asked Feb 05 '15 14:02

Konrad Garus


1 Answers

With Java memory model, is it ever possible that the thread running process will see the effect of its local, unsynchronized write, even though touch happened later?

I see a subtle but important oversimplification going on here: how do you think you would determine that the write action by the second thread occurred exactly between the write and read actions by the first thread? To be able to tell that, you would need to assume that a write action is an idealized point on the time line and that all writes happen in sequential order. On actual hardware, a write is a very complex process involving CPU pipeline, store buffers, L1 cache, L2 cache, the front-side bus, and finally RAM. The same kind of process goes on concurrently on all CPU cores. So what exactly do you mean by one write "happening after" the other?

Further, consider the "Roach Motel" paradigm, which seems to help many people as a sort of a "mental shortcut" into the consequences of the Java Memory Model: your code can legally be transformed into

public void process() {
    doStuff();
    synchronized(this) {
        someoneTouchedMeWhenIWasWorking = false;
        if (someoneTouchedMeWhenIWasWorking) {
            System.out.println("Hey!");
        }
    }
}

This is a completely different approach to exploiting the liberties provided by the Java Memory Model to gain performance. The read inside the if-clause will indeed require an actual memory address read, as opposed to inlining the value of false directly and consequently deleting the whole if-block (this would actually happen without synchronized), but the write of false will not necessarily write through to RAM. In the next step of reasoning the optimizing compiler may decide to delete the assignment to false altogether. Depending on the specifics of all other parts of the code this is one of the things that could happen, but there are many other possibilities as well.

The main message to take away from the above should be: don't pretend you can reason away on Java code using some simplified "local cache invalidation" concept; instead stick to the official Java Memory Model and consider all liberties it offers actually taken.

Is it necessary to synchronize the first write or make the variable volatile?

With the above discussion in mind, I hope you realize that this question actually has no substance. You will either observe the write by the other thread and thus be guaranteed to observe all the other actions it did before that write, or you won't observe it and won't have the guarantees. If your code is free of data races, either outcome will work for you. If it has data races, then better fix that instead of trying to fix your idiom.

Finally, imagine that your variable is volatile. What will that give you that you don't have now? In terms of the JMM formalism the two writes will have a definite order between them (imposed by the total synchronization order), but you will be in exactly the same position as you are now: that definite order will be arbitrary in any specific execution.

like image 96
Marko Topolnik Avatar answered Nov 15 '22 12:11

Marko Topolnik