I know, I know. It's never a JRE issue. But I've been over this code a lot and I still don't see the issue. It comes from Java Synchronization Not Working as Expected from SO and the poster named "Luke". The interesting section is a synchronized class that has the following try/finally but still managed to have to have insideGetBuffer escape as true.
insideGetBuffer = true;
try {
...
} finally {
insideGetBuffer = false;
}
I've boiled the code down to its lowest level and added some comments around the changes that can change the error count. This is pretty repeatable to me with 4/5 runs generating errors. If you can't get it to fail I'd try decreasing the fixed-rate period and increasing the NUMBER_TO_USE constant. I also hear that running it as a junit test versus as a Java application produces is different as well.
I'm using the following Java on a MacBook Pro running 10.7.4.
java version "1.6.0_31"
Java(TM) SE Runtime Environment (build 1.6.0_31-b04-415-11M3635)
Java HotSpot(TM) 64-Bit Server VM (build 20.6-b01-415, mixed mode)
Here's the code. It fails for me whether it is run inside of Eclipse or from the command line. Any idea where the error is? Does it work for you? What JRE version are you using? Thanks.
public class RaceConditionTest {
// if this is decreased it may remove the errors
public static final int NUMBER_TO_USE = 1000000;
private Map<Integer, Buffer> bufferMap = new HashMap<Integer, Buffer>();
public static void main(String[] args) {
new RaceConditionTest().strangeRaceConditionTest();
}
@Test
public void strangeRaceConditionTest() {
final Buffer buffer = getBuffer();
TimerTask getBufferTask = new TimerTask() {
@Override
public void run() {
buffer.getBuffer();
}
};
// if the period is increased it seems to reduce the errors
new Timer(true).scheduleAtFixedRate(getBufferTask, 0 /* delay ms */, 10 /* period ms */);
for (long i = 0; i < NUMBER_TO_USE; i++) {
// if we inline the remove() method here then no errors
// if we change this to buffer.remove() then no errors
remove();
}
assertEquals(0, buffer.getErrorC());
}
private synchronized void remove() {
Buffer buffer = getBuffer();
buffer.remove();
}
private synchronized Buffer getBuffer() {
// if we remove this whole map nonesense then no errors
Buffer buffer = bufferMap.get(1);
// if this test/else is flipped so it is buffer == null ... then no errors
if (buffer != null) {
// if this line is commented out then no errors
buffer = bufferMap.get(1);
} else {
buffer = new Buffer();
bufferMap.put(1, buffer);
}
return buffer;
}
// moving this out to its own class file doesn't seem to help
private static class Buffer {
// these don't have to be volatile, accessed only in synchronized methods
private List<Integer> list = new ArrayList<Integer>();
private boolean insideGetBuffer = false;
private int errorC = 0;
public Buffer() {
// initialize the list
for (long i = 0; i < NUMBER_TO_USE; i++) {
list.add(null);
}
}
public synchronized void remove() {
if (insideGetBuffer) {
// adding a System.out.println here makes the problem more repeatable
// System.out.println("How did we get here?");
errorC++;
}
}
public synchronized void getBuffer() {
insideGetBuffer = true;
try {
// if you comment out this sleep then no errors
Thread.sleep(5);
} catch (Exception e) {
e.printStackTrace();
} finally {
insideGetBuffer = false;
}
}
public synchronized int getErrorC() {
return errorC;
}
}
}
main, it fails every time with ~412k errors. I'm on 1.6.0_24 64-bit on Windows 7 here. – Luke Jun 13 '12 at 18:40volatile, especially the boolean. I have made all variables (other thanNUMBER_TO_USE) volatile and the issue still happens for me. Interestingly, making this change makes the issue happen more reliably for me. – Luke Jun 15 '12 at 15:06volatilebecause it is only changed from within asynchronizedmethod. That already establishes the memory barrier. They are wrong. – Gray Jun 15 '12 at 15:09