JMM- synchronization access in a concrete example.

175 views
Skip to first unread message

John Hening

unread,
Sep 25, 2018, 11:52:14 AM9/25/18
to mechanical-sympathy
public class Test {
   
ArrayList<X> xs;      
   
ArrayList<Doer> doers;
   
Executor executor = Executors.newSingleThreadExecutor();

    static class Doer {
     
public void does(X x){
           x
.f();                                                         // (2)
     
}
   
}

   
void test() {
       
for(X x : xs){
            x
.f();                                                      // (1)
       
           
for(Doer d : doers) {
                executor
.execute(() -> d.does(x));
           
}
       
}
   
}
}




For my eye, if X.f is not synchronized it is incorrect because of two facts (and only that two facts):

1. Obviously, there is data race between (1) and (2). There are no more data races here. (doers contains different objects)
2. There is no guarantee that (1) will be executed before (2). Yes?

If X.f would be synchronized that code will be correct because:
1. There is no data race.
2. There is guarantee that (1) will be executed before (2) because (1) is a synchronization action and Executor.execute is also a synchronization access (not specifically execute itself)

Yes?

Cezary Biernacki

unread,
Sep 25, 2018, 2:27:23 PM9/25/18
to mechanica...@googlegroups.com
Hi,
I assume you are worried that (1) can be moved by the compiler below "executor.execute()"  or on processors with weak memory models changes from (1) might not be fully visible to other threads  when (2) is executed. I believe it is not going to happen, as (1) should happen before "executor.execute()" (because they are on the same thread), which in turn should happen before retrieving a task for execution (because they are synchronised via task queue), which again happens before (2) (because on the same thread with the retrieving operation).

Best regards,
Cezary
 

--
You received this message because you are subscribed to the Google Groups "mechanical-sympathy" group.
To unsubscribe from this group and stop receiving emails from it, send an email to mechanical-symp...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.


--
Cezary Biernacki
Director of Software Development
crosswordcybersecurity.com @crosswordcyber 

Crossword Cybersecurity Plc is a public limited company registered in England and Wales. Registered number: 08927013. Registered office: 6th Floor, 60 Gracechurch Street, London, EC3V 0HR.


John Hening

unread,
Sep 25, 2018, 2:44:32 PM9/25/18
to mechanical-sympathy
(1) should happen before "executor.execute()" (because they are on the same thread),

It does not matter that they are executed on the same thread. I do not see cause here to HB relation was set up.

Cezary Biernacki

unread,
Sep 25, 2018, 3:06:30 PM9/25/18
to mechanica...@googlegroups.com
My guess because "If x and y are actions of the same thread and x comes before y in program order, then hb(x, y)." https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.5

On Tue, Sep 25, 2018 at 8:44 PM John Hening <goci...@gmail.com> wrote:
(1) should happen before "executor.execute()" (because they are on the same thread),

It does not matter that they are executed on the same thread. I do not see cause here to HB relation was set up.

--
You received this message because you are subscribed to the Google Groups "mechanical-sympathy" group.
To unsubscribe from this group and stop receiving emails from it, send an email to mechanical-symp...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Tom Lee

unread,
Sep 25, 2018, 3:15:48 PM9/25/18
to mechanica...@googlegroups.com
Also from the Executor docs: https://docs.oracle.com/javase/10/docs/api/java/util/concurrent/Executor.html

Memory consistency effects: Actions in a thread prior to submitting a Runnable object to an Executor happen-before its execution begins, perhaps in another thread.
 
Tom Lee http://tomlee.co / @tglee

Ben Evans

unread,
Sep 25, 2018, 3:57:58 PM9/25/18
to mechanica...@googlegroups.com
This is a really useful additional example of a HB relationship beyond
the usual ones, albeit one that you would really hope was true.

John Hening

unread,
Sep 25, 2018, 4:17:14 PM9/25/18
to mechanical-sympathy
My guess because "If x and y are actions of the same thread and x comes before y in program order, then hb(x, y)." https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.5


But please note that:

"Among all the inter-thread actions performed by each thread t, the program order of t is a total order that reflects the order in which these actions would be performed according to the intra-thread semantics of t. "

(https://docs.oracle.com/javase/specs/jls/)

So don't be sure that there is HB between (1) and executor.execute()


Memory consistency effects: Actions in a thread prior to submitting a Runnable object to an Executor happen-before its execution begins, perhaps in another thread.


This is interesting. Does it mean that both version presented in the first post are correct? Does it mean that it is an additional guarantee of JVM? I sitll don't see how we can conclude that guarantee from JMM.

Tom Lee

unread,
Sep 25, 2018, 4:47:57 PM9/25/18
to mechanica...@googlegroups.com
This is interesting. Does it mean that both version presented in the first post are correct?

I'm probably not qualified to talk about whether it's "correct", but it certainly seems "less incorrect" than it would be if Executor.execute() didn't have the happens-before guarantee.

Does it mean that it is an additional guarantee of JVM? I sitll don't see how we can conclude that guarantee from JMM.

Maybe I'm misunderstanding what you're asking here, but I think the API docs just imply that implementations of Executor are responsible for enforcing a happens-before using the JMM rather than Executor.execute() itself being somehow "special" in the eyes of the JMM/JVM. For example if we talk about Executors in general, a thread pool Executor is usually fronted by a synchronized queue (or a disruptor or similar) which offers its own happens-before guarantees via the JMM. Or perhaps the underlying implementation creates a brand new thread per Runnable & through the magic of a rule in https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.5 ("A call to start() on a thread happens-before any actions in the started thread.") you get a happens-before that way.

If the underlying implementation fails to implement those semantics somehow then it seems like they'd be breaking the contract & I'd go so far as to call it a bug. Perhaps somebody knows differently, but I can't imagine the JVM/JMM itself can or should treat Executor.execute() in any special way.

--
You received this message because you are subscribed to the Google Groups "mechanical-sympathy" group.
To unsubscribe from this group and stop receiving emails from it, send an email to mechanical-symp...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Gil Tene

unread,
Sep 25, 2018, 10:49:21 PM9/25/18
to mechanical-sympathy
As Tom noted, The Executor's submission happens-before promise prevents a reordering of (1) and (2) above.

Note that, as written, the reason you you don't have data races between (2) and (2) is that executor is known to be a single threaded executor (and will only run one task at a time). Without that quality, you would have plenty of (2) vs. (2) races. It is not that "doers contain different objects": your code submits executions of functions using the same x member of xs to all doers, and it is only the guaranteed serialization in your chosen executor implementation that prevents x,f()s from racing on the same x...

John Hening

unread,
Sep 26, 2018, 1:34:43 AM9/26/18
to mechanica...@googlegroups.com
Tom,

Actually you right. I get it!

Gil,
thanks for your note. You obviously right. If I use a multithreaded executor I get a lot races in a result.
So, does it mean that my both version of example are correct?

How to interpret a citation given by Cezary?: "If x and y are actions of the same thread and x comes before y in program order, then hb(x, y)."
For my eye the key is in interpreting of program order. So, if we have two statements [X, Y] and order of execution does not matter because both are intrathread-consistent it means that [Y,X] are in program order and HB(Y,X) by a rule I cite above.

So, If we had no Executor's (and no other) guarantee it could be reordered, yes?

Tom Lee

unread,
Sep 26, 2018, 2:09:41 AM9/26/18
to mechanica...@googlegroups.com
How to interpret a citation given by Cezary?: "If x and y are actions of the same thread and x comes before y in program order, then hb(x, y)."

This is a great question! Hopefully I'm not too far off-base but I think it comes down to effects/"visibility" and this statement hints at the kinds of optimization that might be legal. Assume a single thread of execution where reordering is in play. Instructions implementing the actions can be reordered in such a way that we perform all the work required to execute actions x and y, but until the effects of those operations "leak" you're still not strictly violating the hb(x, y) rule.

For example, sticking to the "single thread of execution" example and local variables:

a: x =1
b: y = 2
c: z = x + y * 3
d: y = z
e...: (use x and y somehow)

We could reorder the underlying instructions such that we compute z directly & simply omit the assignment to x and y such that what actually gets executed by the CPU looks more like:

a: (optimized away)
b: (optimized away)
c: z = 3 * 2 + 1
d: y = z
e: (use x and y somehow)

Does it matter that we've eliminated the assignments and reordered some of the arithmetic? Probably not: we've changed the underlying instructions but as far as the effects are concerned we ultimately "see" hb(a,b) -> hb(b,c) -> hb(c,d) -> hb(d,e...) and a correct execution of our program! Of course, this is easier to reason about when the instructions/actions involved are simple. Replace simple arithmetic with method calls, volatile reads & writes, allocation, threads, etc. and it obviously gets a whole lot messier.

(No doubt somebody out there can formalize what I'm trying to get across here, I'm sort of running on intuition. :))

On Tue, Sep 25, 2018 at 10:34 PM John Hening <goci...@gmail.com> wrote:
Tom,

Actually you right. I get it!

Gil,
thanks for your note. You obviously right. If I use multithreaded executor I got a lot races in a result.
So, does it mean that my both version of example are correct?

How to interpret a citation given by Cezary?: "If x and y are actions of the same thread and x comes before y in program order, then hb(x, y)."
For my eye the key is in interpreting of program order. So, if we have two statements [X, Y] and order of execution does not matter because both are intrathread-consistent it means that [Y,X] are in program order and HB(Y,X) by a rule I cite above.

So, If we had no Executor's (and no other) guarantee it could be reordered.


W dniu środa, 26 września 2018 04:49:21 UTC+2 użytkownik Gil Tene napisał:
As Tom noted, The Executor's submission happens-before promise prevents a reordering of (1) and (2) above.

Note that, as written, the reason you you don't have data races between (2) and (2) is that executor is known to be a single threaded executor (and will only run one task at a time). Without that quality, you would have plenty of (2) vs. (2) races. It is not that "doers contain different objects": your code submits executions of functions using the same x member of xs to all doers, and it is only the guaranteed serialization in your chosen executor implementation that prevents x,f()s from racing on the same x...

On Tuesday, September 25, 2018 at 8:52:14 AM UTC-7, John Hening wrote:
public class Test {
   
ArrayList<X> xs;      
   
ArrayList<Doer> doers;
   
Executor executor = Executors.newSingleThreadExecutor();

    static class Doer {
     
public void does(X x){
           x
.f();                                                         // (2)
     
}
   
}

   
void test() {
       
for(X x : xs){
            x
.f();                                                      // (1)
       
           
for(Doer d : doers) {
                executor
.execute(() -> d.does(x));
           
}
       
}
   
}
}




For my eye, if X.f is not synchronized it is incorrect because of two facts (and only that two facts):

1. Obviously, there is data race between (1) and (2). There are no more data races here. (doers contains different objects)
2. There is no guarantee that (1) will be executed before (2). Yes?

If X.f would be synchronized that code will be correct because:
1. There is no data race.
2. There is guarantee that (1) will be executed before (2) because (1) is a synchronization action and Executor.execute is also a synchronization access (not specifically execute itself)

Yes?

--
You received this message because you are subscribed to the Google Groups "mechanical-sympathy" group.
To unsubscribe from this group and stop receiving emails from it, send an email to mechanical-symp...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Tom Lee

unread,
Sep 26, 2018, 2:11:14 AM9/26/18
to mechanica...@googlegroups.com
Oops: "use x and y somehow" should read "use y and z somehow" in the example I gave. Hopefully that's not too confusing.
Reply all
Reply to author
Forward
0 new messages