How to Provoke a Race Condition

Zigurd Mednieks
97 Things
Published in
3 min readJul 6, 2019

You have a method used by multiple threads in a program. You suspect there’s a race condition, but the evidence is inconclusive. Nor does the evidence tell you what’s wrong.

This method stores information in two collections:

public boolean make(UUID belongsTo) {
Thing thing = new Thing(belongsTo);
sThingsOne.add(thing);
thing = new Thing(belongsTo);
return sThingsTwo.add(thing);
}

The collections are synchronized:

ArrayList<Thing> thingsOne = new ArrayList<Thing>();
ArrayList<Thing> thingsTwo = new ArrayList<Thing>();
Collection<Thing> sThingsOne =
Collections.synchronizedCollection(thingsOne);
Collection<Thing> sThingsTwo =
Collections.synchronizedCollection(thingsTwo);

But, occasionally, when you get a pair of Thing objects, the belongsTo value is wrong. Stepping through the code in the debugger won’t help. A race condition happens by chance. You need to improve your chances.

First, let’s make lots of threads using ExecutorService, which lets you create a thread pool and execute a set of tasks:

List<Callable<Object>> callableTasks = new ArrayList<>();
System.out.println("Creating tasks");
for (int i = 0; i < taskCount; i++) {
callableTasks.add(Executors.callable(runnableTask));
}
System.out.println("Invoking tasks");
try {
pool.invokeAll(callableTasks);
} catch (Exception ex) {}
pool.shutdown();
try {
pool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {}

This code takes a thread pool and a task that calls our suspect code. It invokes all the tasks. By changing the values of taskCount and the size of the thread pool, you can invoke a very large number of concurrent threads. This code tells you if something went wrong:

for (int i = 0; i < taskCount; i++) {
Thing things[] = maker.get(i);
if (things[0].belongsTo.equals(things[1].belongsTo)) {
System.out.println("Same!");
} else {
// The two Things do not belong to the same "owner"
System.out.println("Ruh Roh!");
}
}

But what if running the test still doesn’t reveal a problem? You can increase the number of threads in the thread pool or increase the number of tasks. But sometimes you need to cause thread-switching more than would happen by chance.

Do it by inserting a call to Thread.sleep, which will cause a thread to be descheduled.

public boolean make(UUID belongsTo) {
Thing thing = new Thing(belongsTo);
sThingsOne.add(thing);
try {
Thread.sleep((long)(Math.random() * 100));
} catch (InterruptedException e) {}
thing = new Thing(belongsTo);
return sThingsTwo.add(thing);
}

This results in repeatable tests that fail, even with a modest number of threads and tasks.

While the collections are thread-safe, should a thread be preempted, you can end up with different values of belongsTo stored alongside each other.

Two tools are needed for finding thread problems: test code that runs lots of threads, and, sometimes, explicitly causing your suspect code to cause a thread to be descheduled.

To fix this code, you can declare the make method synchronized. Or you can create a synchronized block around the two calls to add. Can you think of other approaches?

Try playing around with this code to see what happens and why (a ready-made example can be found at https://repl.it/@zigurd_mednieks/97ThingsExample). Try changing the thread-pool size and the number of tasks. Vary the range of random milliseconds in the call to Thread.sleep to see if there is a threshold sleep time for seeing a significant number of failed tests. Experimentation will get you more comfortable with intentionally changing the way thread execution behaves. That will make you better at finding and diagnosing problems with concurrent code.

--

--

Zigurd Mednieks
97 Things
Writer for

Zigurd is a consultant and author. He has been a CTO, engineering team leader, product manager, and program manager in mobile software, telecommunications…