March 11, 2016
Detecting java race conditions is hard, detecting them during tests is impossible. Really?
In the following I want to show you, that it is actually rather easy. In this first part you will see how to detect lost updates, the first type of java race conditions. In the second part we will look at the second type of java race conditions: Non atomic access.
If you read and write to the same field from different threads without synchronization, you will lose updates.
Why? In multi core computers every core has a cache. If you write to a field, another thread sees the old value if the thread runs on another core. Or if the thread runs on the same core, you will see the new value. Only if the cache was invalidated, the thread will always see the new value. Without invalidating the cache you lose updates. And invalidating the cache is what a synchronization statement or writing to a volatile field does.
Simply run a multithreaded test. Afterwards check if each concurrent field access is correctly synchronized. The java memory model formally specifies which statements correctly synchronize a field access. For example the following is correctly synchronized since between the write and read is a thread start:
public class ThreadStart extends Thread { private int i = 0; public static void main(String[] args ) { ThreadStart threadStart = new ThreadStart(); threadStart.i = 8; /* * * see https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.4 * An action that starts a thread synchronizes-with the first action in the thread * it starts. */ threadStart.start(); } @Override public void run() { int j = i; } }
So let’s get started with a simple example, a counter:
public class Counter { private int i = 0; public void addOne() { i++; } }
To test the counter, we use the following junit test. The concurrent test runner runs the test method from four different threads.
import org.junit.Test; import org.junit.runner.RunWith; import com.anarsoft.vmlens.concurrent.junit.ConcurrentTestRunner; @RunWith(ConcurrentTestRunner.class) public class TestCounter { private final Counter counter = new Counter(); @Test public void testAdd() { counter.addOne(); } }
To trace the fields and synchronization actions, add the vmlens agent path to the virtual machine arguments. After the run, vmlens checks all field accesses. If vmlens finds a field access which is not correctly synchronized, we have detected a java race condition.
Probably you have noticed the detected java race condition accessing the field numInvocations in the class sun.reflect.NativeMethodAccessorImpl. If we look at the source code, we see that it is used to switch to a faster implementation, if a threshold is reached.
public Object invoke(Object obj, Object[] args) throws IllegalArgumentException, InvocationTargetException { if (++numInvocations > ReflectionFactory.inflationThreshold()) { MethodAccessorImpl acc = (MethodAccessorImpl) new MethodAccessorGenerator(). generateMethod(method.getDeclaringClass(), method.getName(), method.getParameterTypes(), method.getReturnType(), method.getExceptionTypes(), method.getModifiers()); parent.setDelegate(acc); } return invoke0(method, obj, args); }This is an example for a java race condition, which is not a bug but a feature. The performance loss to make the numInvocations count thread safe is worse than to lose some values.
Since detecting java race conditions with testing is rather new, you can expect many undetected race conditions. Here is an example of race conditions during the start and stop of Jenkins, an open source continuous integration server:
So detecting lost updates with tests is possible. As test runner I used concurrent-junit, as race condition catcher I used vmlens. In the next part we will look at testing non atomic updates.
© 2020 vmlens Legal Notice Privacy Policy