How to test multi-threaded and concurrent Java
In the following tutorial, I will show you how to use VMLens with a simple example.
We will build a concurrent Address
class that holds a street and a city.
The class should support parallel reading and writing from multiple threads.
You can download all examples from this git repository.
The first problem: A data race
Here is our first implementation of the Address:
public class Address {
private String street;
private String city;
public Address(String street, String city) {
this.street = street;
this.city = city;
}
public void update(String street, String city) {
this.street = street;
this.city = city;
}
public String getStreetAndCity() {
return street + ", " + city;
}
}
And the test:
@Test
public void readWrite() throws InterruptedException {
try(AllInterleavings allInterleavings = new AllInterleavings("howto.address.regularFieldReadWrite")) {
while (allInterleavings.hasNext()) {
// Given
Address address = new Address("First Street", "First City");
// When
Thread first = new Thread() {
@Override
public void run() {
address.update("Second Street","Second City");
}
};
first.start();
String streetAndCity = address.getStreetAndCity();;
first.join();
// Then
assertThat(streetAndCity,anyOf(is("First Street, First City"),
is("Second Street, Second City")));
}
}
}
The test updates the Address in a newly started thread, while simultaneously reading the Address inside the original thread. We expect to either read the original address if the read happens before the update. Or the updated address if the read happens after the update. We test this using the assertion:
assertThat(streetAndCity,anyOf(is("First Street, First City"),
is("Second Street, Second City")));`
To cover all possible thread interleavings, we wrap the test in a while loop that runs through every possible execution order:
try(AllInterleavings allInterleavings = new AllInterleavings("howto.address.regularFieldReadWrite")) {
while (allInterleavings.hasNext()) {
Running the test with VMLens leads to the following error: a data race.

A data race occurs when two threads access the same field at the same time without proper synchronization. Synchronization actions — such as reading or writing a volatile field or using a lock — ensure visibility and ordering between threads. Without synchronization, there’s no guarantee that a thread will see the most recently written value. This is because the compiler may reorder instructions, and CPU cores can cache field values independently. Both synchronization actions and data races are formally defined in the Java Memory Model.
As the trace shows, there is no synchronization action between the read and write to our street
and city
variables. So VMLens reports a data race. To fix the error, we add a volatile modifier to both fields.
The second problem: A read-modify-write race condition
Here is the new Address class with the two volatile fields:
public class Address {
private volatile String street;
private volatile String city;
public Address(String street, String city) {
this.street = street;
this.city = city;
}
public void update(String street, String city) {
this.street = street;
this.city = city;
}
public String getStreetAndCity() {
return street + ", " + city;
}
}
When we run our test now, the assertion fails:
Expected: (is "First Street, First City" or is "Second Street, Second City")
but: was "First Street, Second City"
We read a partially updated Address. The VMLens report reveals the specific thread interleaving that caused this error:

The main thread first reads the street
variable before it has been updated.
Meanwhile, another thread updates both the street
and city
variables.
When the main thread later reads the city
variable,
it ends up seeing a partially updated Address
. To solve this, we use a ReenatrantLock
.
The solution: A Lock
For our new test, we add a ReenatrantLock
to the update
and getStreetAndCity
methods:
public class Address {
private final Lock lock = new ReentrantLock();
private String street;
private String city;
public Address(String street, String city) {
this.street = street;
this.city = city;
}
public void update(String street, String city) {
lock.lock();
try{
this.street = street;
this.city = city;
} finally {
lock.unlock();
}
}
public synchronized String getStreetAndCity() {
lock.lock();
try{
return street + ", " + city;
} finally {
lock.unlock();
}
}
}
Now our test succeeds.
What to test?
When we write a concurrent class, we want the methods of the class to be atomic. This means that
we either see the state before or after the method call.
We already tested this for the parallel execution of updating and reading our Address
class.
What is still missing is a test for the parallel update of our class from two threads.
This second test is shown below:
@Test
public void writeWrite() throws InterruptedException {
try(AllInterleavings allInterleavings = new AllInterleavings("howto.address.lockWriteWrite")) {
while (allInterleavings.hasNext()) {
// Given
Address address = new Address("First Street", "First City");
// When
Thread first = new Thread() {
@Override
public void run() {
address.update("Second Street","Second City");
}
};
first.start();
address.update("Third Street","Third City");
first.join();
// Then
String streetAndCity = address.getStreetAndCity();
assertThat(streetAndCity,anyOf(is("Second Street, Second City"),
is("Third Street, Third City")));
}
}
}
This test also succeeds for the class with the ReenatrantLock
.
Tests are missing
The number of cores of the CPU is continuously increasing. In 2020, the processor with the highest core count was the AMD EPYC 7H12 with 64 cores and 128 hardware threads. Today, June 2025, the processor with the highest core count has 288 efficiency cores, the Intel Xeon 6 6900E. AMD increased the core count to 128 and 256 hardware threads with the AMD EPYC 9754.
Java with volatile fields, synchronization blocks
and the powerful concurrency utilities in java.util.concurrent
allows us to use all those cores efficiently.
Project Loom with its virtual threads and structured concurrency
will further improve this.
What is still missing is a way to test that we are using all those techniques correctly.
I hope VMLens can fill this gap. Get started with testing multi-threaded, concurrent Java here.