How to test multi-threaded and concurrent Java

How to test multi-threaded and concurrent Java

June 10, 2025

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.

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:

Read Write Race

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.

CPU Plot

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.