How are multi-threaded tests in Java possible?

November 26, 2019

Category: The package java.util.concurrent.atomic

How are multi-threaded tests in Java possible?

Testing multi-threades software seams impossible. Bugs depend on a specific timing of events, sometimes also on specific compiler or microprocessor optimizations. So how is it possible to write tests for multi-threaded software in Java?

Here comes the Java Memory Model into the play. The Java Memory Model is part of the specification of the Java Virtual Machine. It is described here.

The Java Memory Model defines rules to correctly synchronize our application. As long as we follow those rules our application behaves the same on any plattform a Java Virtual Machine is running. So basically it allows us to write multi-threaded software which is plattform independent.

But not only this. It also allows us to write a test for this multi-threaded software.

The Java Memory Model defines synchronization actions like locking a monitor or reading and writing of a volatile variable. And it guarantees that if all reads and writes to shared variables can be ordered through those synchronization actions there is a total order over all individual actions (such as reads and writes) which is consistent with the order of the program, and each individual action is atomic and is immediately visible to every thread.

On the other side, if the reads and writes to shared variables can not be ordered, a data race exists in the program. A data race means that the order is not defined by the Java Memory Model. Components like the compiler or the CPU can reorder the reads and writes in nonintuitive ways and the threads can read stale values.

The idea to test multi-threaded software is first to check for those data races. If such data race exists, the test contains an error. And second test all relations of synchronization actions. For example if we have one thread reading and on thread writing to the same volatile variable. Then we must execute a test run where first the reading thread reads and then the writing thread writes, and a test run where first the writing thread writes and then the reading thread reads.

How to test all thread interleavings?

So to test all interleavings in a systematic way we need to:

  1. For each related sync action a and b create a run with a before b and b before a. Two synchronization actions are related if they can create a synchronized-with relation defined by the JavaMemory Model. For example, a write to a volatile variable creates a synchronized-with relation with a subsequent read from this variable. So the read and the write to the same volatile variable are related.
  2. Check for data races and flag them as error

Let us look at an example to see how this works in practice. Let us use one thread, thread A, to write to volatile variable v and one thread, thread B, to read from this volatile variable. We have the following two thread interleavings:

First Thread A than B

  • Thread A writes to v
  • Thread B reads from v
And second, thread B than A:
  • Thread B reads from v
  • Thread A writes to v

Only the first interleaving, thread A writes than thread B reads, creates a synchronized with relation. If we add a read and write of a normal variable we will see that the second interleaving leads to data races.

Let us see how this looks like by letting Thread A write to the normal field n before writing to the volatile variable v. And let thread B first write from the volatile variable v and then read from the normal field n. Now we have the following four potential thread interleavings:

First:

  • Thread A writes to n
  • Thread A writes to v
  • Thread B reads from v
  • Thread B reads from n
Second:
  • Thread B reads from v
  • Thread A writes to n
  • Thread A writes to v
  • Thread B reads from n
Third:
  • Thread A writes to n
  • Thread B reads from v
  • Thread A writes to v
  • Thread B reads from n
Fourth:
  • Thread A writes to n
  • Thread B reads from v
  • Thread B reads from n
  • Thread A writes to v

Only the first interleaving is data race free.

The example shows to find all data races we need to test all relations between related sync actions. So the two parts, testing of all sync action and checking for data races depend on each other. To find all data races we need to create runs for every combination of related sync actions. And to make sure that our test and thereby our program does not depend on external components like the uses compiler or CPU our program must be data race free.

How to write such tests?

The above described two steps to test multi-threaded software are implemented in vmlens. vmlens traces a test using a java agent with bytecode transformation.
It records each synchronization action and memory access to find data races. And it calculates the different thread interleavings using the recorded synchronization actions. To enforce a specific thread interleaving vmlens inserts wait instruction in to the tested bytecode. In practice a test with vmlens looks like this:

Summary

The Java Memory Model allows us to write a test for this multi-threaded software. It defines which thread interleavings exist and gives us a way to create all different thread interleavings.

Make your application thread safe

LEARN MORE

© 2020 vmlens Legal Notice Privacy Policy