From: Knute Johnson on 13 Mar 2010 21:58 On 3/13/2010 5:18 PM, Peter Duniho wrote: > Knute Johnson wrote: >> [...] The one point that I'm not sure about is the call to repaint() >> synchronizing data with the EDT. Looking at the code I don't see how >> calling repaint() could create any synchronization. All that happens >> is that some code is run on the EDT. No threads are started or joined. >> I couldn't find anything in the docs that implied this either. > > It is a consequence of how repaint() works. There are two possibilities: > > – You call repaint() in the EDT > – You call repaint() in a thread other than the EDT > > In the first case, it should be obvious that whatever code executes in > that same thread before you call repaint() must produce observable > results in the same thread in the paintComponent() call that occurs as a > result of the repaint() method. > > So, it's the second case that's interesting. > > The basic thing to understand is that all thread synchronization relies > on memory barriers (that is, a sort of "checkpoint" that ensures that > reads and writes cannot be moved across the barrier, > execution-order-wise and time-wise; any read or write that is, in > program order, on one side of the barrier or the other must remain on > that side of the barrier in execution order). > > Without memory barriers, it's impossible to synchronize threads. But, > memory barriers don't just force the specific data being affected to be > mutually observable between threads by some specific synchronization > construct (like "volatile"). They affect _all_ data used by the > cooperating threads. > > Think about the "volatile" keyword, which is one of the weakest > synchronization constructs available. Even that construct provides a > guarantee that _all_ data written by a given thread before (in program > order) that thread writes to the volatile variable will be observed by > any thread reading that data _after_ (again, in program order) reading > from the same volatile variable, if the read of the volatile variable > occurs after (in execution order) the given write. > > Put another way, if we have variables "int i" and "volatile int j", then > if we have threads executing like this: > > thread 1 thread 2 > –––––––– –––––––– > i = 0; > j = 0; > while (j != 10) { } > i = 5; > j = 10; > System.out.println(i); > > The output is guaranteed to be "5". That's because "volatile" guaranteed > not only that thread 2 would see the modification to "j", but also that, > since the write to "i" happened before "j" was written, and the read > from "i" happened _after_ the write to "j" was observed, the new value > of "i" is also observed. > > Now, think about what repaint() has to do when called from a thread > other than the EDT. It necessarily has to cause a call to the > paintComponent() method to happen; that's its job. > > In doing so, it has to synchronize between the two threads such that the > EDT sees a change in state written by the other thread as a result of > the call to repaint(). This synchronization, whether caused by a > volatile variable, or a notify(), or whatever, will necessarily involve > a memory barrier. > > This means that any change to variable state that occurs in the other > thread before the call to repaint() _must_ be observed by any code > executing in the EDT after the synchronization of the data involved in > the call to repaint(). > > And since the paintComponent() call that happens as a result of that > call to repaint() obviously can't happen until after that > synchronization takes place, it means that any code in the > paintComponent() method _must_ be able to see any change in state caused > by the other thread that called repaint(), because that code _also_ > necessarily executes after the synchronization takes place. > > Note that the guarantee is only with respect to the two threads that are > synchronized with each other. I'm not an expert in this area, and I > don't know whether any _other_ threads are also guaranteed to be able to > observe state change occurring in the EDT and the thread calling > repaint(), unless they too also participate in the synchronization (e.g. > include their own memory barrier). I suspect not, but I don't know for > sure. > > But fortunately, that part doesn't matter. The only thing you really > need to worry about are the threads that do synchronize with each other, > and it happens that specifically because of the nature of that > synchronization, you can be assured that writes happening in one thread > before the synchronization point will be observed by reads happening in > a different thread after that same synchronization point. It's a direct > consequence of the whole reason for having the synchronization point > (i.e. that's specifically what the synchronization point does). > > Hope that helps! > > Pete Pete: Thanks very much. I understand what you are saying I just don't understand what it is about repaint() that could cause this memory barrier. All repaint() does is queue some Runnables up for the EDT. No threads are started or joined. So what is the barrier? The really unfortunate thing about all this is that there is now way to test any of it. I've never been able to get one thread not to be able to read data written by another. Thanks again for all your input, it has been very valuable. -- Knute Johnson email s/nospam/knute2010/
From: Peter Duniho on 14 Mar 2010 00:16
Knute Johnson wrote: > Thanks very much. I understand what you are saying I just don't > understand what it is about repaint() that could cause this memory > barrier. All repaint() does is queue some Runnables up for the EDT. No > threads are started or joined. So what is the barrier? To queue a Runnable for the EDT, repaint() called in a thread other than the EDT must touch data that is also used by the EDT. The only way for it to do that is to synchronize with the EDT. That synchronization implies a memory barrier. > The really unfortunate thing about all this is that there is now way to > test any of it. I've never been able to get one thread not to be able > to read data written by another. I agree. One of the awful things about dealing with multi-threaded issues is that it's very difficult to create reproducible scenarios. This is made even more difficult in a cross-platform environment like Java by the fact that various hardware architectures have very different memory models. On Intel's x86 line, there's built-in volatile semantics on the CPU, so all that "volatile" should do in that context is inhibit certain compiler optimizations; it's hard or impossible to see memory coherency problems, because the architecture doesn't allow that. And then on top of all that, there's the problem that multi-threaded programming is by its very nature non-deterministic. You can have buggy multi-threaded code that works fine for years before it blows up. Certain kinds of bugs can be simulated through explicit management of thread scheduling, but doing that within the user code introduces the very memory barriers that "fix" (i.e. hide) other kinds of bugs. What we really need is a Java version that can, at the JVM level, simulate the various memory coherency problems that can occur if you fail to synchronize code correctly, and do so in a reliable, deterministic way (that is, force behavior that will, if a bug exists, expose the bug). > Thanks again for all your input, it has been very valuable. Happy to help! Pete |