Summary
I recently noticed in JRE1.5 that HotSpot is yanking loop controls as invariant when not marked as volatile. Have you encountered this?
Advertisement
The Java Memory Model was formally updated in JSE1.5 to include a strict definition and implementation of the volatile keyword. In JSE1.4, Sun had already started working on making their VM compatible with the better specification of synchronized and volatile that would be implemented in JSE1.5. Recently, I started using a P4 based laptop with Hyper-Threading, that now makes my software run in a multi-processor environment. This changes everything...
After starting to use the new machine, I noticed that I started getting bit by an optization where loop control, boolean value tests are being yanked out of the loop.
The basic structure of the problematic software is as follows.
class MyClass ... implements Runnable {
private boolean stopping;
public void stop() {
stopping = true;
// maybe close a socket etc.
}
public void run() {
while( !stopping ) {
try {
... do some work ...
} catch( Exception ex ) {
... process exception ...
}
}
}
}
What I am seeing is that HotSpot is apparently (this I have gleened from conversations
on Doug Lea's Concurrency-Interest list) yanking the while( !stopping) { }
block out and replacing it with an if( !stopping ) { } block surrounding a while(true) { } block!
class MyClass ... implements Runnable {
private boolean stopping;
public void stop() {
stopping = true;
// maybe close a socket etc.
}
public void run() {
if( !stopping ) {
while(true) {
try {
... do some work ...
} catch( Exception ex ) {
... process exception ...
}
}
}
}
}
The end result, of course, is that a call to stop(); will never be able to stop the looping thread executing in run(){ }.
What I've found, of course, is that if I go ahead and declare stopping as volatile that suddenly the code works as expected!
I ran this code for a long time on a single processor laptop on JSE1.5 even, and don't recall that I saw this
happening. I wonder if this optimization only happens on MP machines.
So, beware... If you have been using loop controls like this and have neither made the references happen inside synchronized code, or made the variables volatile, you may be getting bit by this "optimization!"
I am assuming that stop() and stopping are not referred to by the run method or any of it's method calls. If that is the case, I would hope adding such a reference would defeat this optimization. I can test it later but if you have a minute can you give it a try.
The only way I can think that this optimization is being done is through some sort of escape analysis. I thought the escape analysis features that allow this kind of optimization and stack-based allocation were not going to be introduced until 1.6.
> So, beware... If you have been using loop controls like > this and have neither made the references happen inside > synchronized code, or made the variables volatile, you may > be getting bit by this "optimization!"
I agree this can be a serious concern. On the other hand, this kind of code has been known to be unsafe for years. That it ever worked is only because of JVM lenience. For a while now (if not always) it's been the case that threads can cache this kind of value and that there is no requirement to flush, ever. Only by using synchronization marking the field volatile was a flush required. My understand was that there was some issue with volatile not being an absolute guarantee as it should have been.
Is it possible that you are seeing a cache issue and there is no optimization? In other words, how did you determine this is what is happening?
> > So, beware... If you have been using loop controls > like > > this and have neither made the references happen inside > > synchronized code, or made the variables volatile, you > may > > be getting bit by this "optimization!" > > I agree this can be a serious concern. On the other hand, > this kind of code has been known to be unsafe for years. > That it ever worked is only because of JVM lenience. For > a while now (if not always) it's been the case that > threads can cache this kind of value and that there is no > requirement to flush, ever. Only by using synchronization > marking the field volatile was a flush required. My > understand was that there was some issue with volatile not > being an absolute guarantee as it should have been.
The places where I am find this is in old client code that would disconnect socket listening threads or shutdown other similar activity in other threads. It always seemed to work fine until I got the P4 w/HT laptop.
> Is it possible that you are seeing a cache issue and there > is no optimization? In other words, how did you determine > this is what is happening?
Given that it started being very noticable in the P4 w/HT laptop, it might be that it is actually a caching issue more than an optimization. Since it's happening in HotSpot, I am not sure how to tell exactly what the true flow of execution is.
I am guessing that this is the loop invariant optimization occuring based on past coversations on the concurrency-interests list. But, it is more of a guess what the cause is. The change to using volatile, or putting synchronized around the references and writes solves the problem, for either potential source.
> > Is it possible that you are seeing a cache issue and > there > > is no optimization? In other words, how did you > determine > > this is what is happening? > > Given that it started being very noticable in the P4 w/HT > laptop, it might be that it is actually a caching issue > more than an optimization. Since it's happening in > HotSpot, I am not sure how to tell exactly what the true > flow of execution is. > > I am guessing that this is the loop invariant optimization > occuring based on past coversations on the > concurrency-interests list. But, it is more of a guess > what the cause is. The change to using volatile, or > putting synchronized around the references and writes > solves the problem, for either potential source.
It's also possible that HotSpot is caching more aggresively in 1.5, possibly because of the improved memory model.
Regardless of implementation peculiarities, if you EVER have two threads accessing the same variable, you need to declare that variable volatile (or use other synchronization features). Loop optimizations do not really enter into it.
Volatile tells the compiler/vm to make sure that the variable doesn't get cached. For example... cached in the per-cpu cache on a multiple-cpu machine.
Forgetting to declare a variable volatile in a single-cpu machine, you won't see a problem, but a multiple-cpu machine, you may or may not see a problem. Just do what the language-spec tells you to do and declare it volatile.