Mutexes

In the previous example, we saw how to manage simple counter state using atomic operations. For more complex state, we can use a mutex to safely access data across multiple threads.

Container holds a map of counters; since we want to update it concurrently from multiple threads, we add a ReentrantLock to synchronize access. Note that locks must not be copied, so if this class is passed around, it should be done by reference.

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

class Container {
    private final ReentrantLock lock = new ReentrantLock();
    private final Map<String, Integer> counters = new HashMap<>();

    public void inc(String name) {
        lock.lock();
        try {
            counters.put(name, counters.getOrDefault(name, 0) + 1);
        } finally {
            lock.unlock();
        }
    }

    public Map<String, Integer> getCounters() {
        return counters;
    }
}

Container holds a map of counters. To modify counters safely, we use the ReentrantLock to lock the critical section of code.

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Container container = new Container();
        container.inc("a");
        container.inc("a");
        container.inc("b");

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                container.inc("a");
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                container.inc("a");
            }
        });

        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                container.inc("b");
            }
        });
        
        t1.start();
        t2.start();
        t3.start();

        t1.join();
        t2.join();
        t3.join();

        System.out.println(container.getCounters());
    }
}

This function increments a named counter in a loop.

Threads are used to run several tasks concurrently, and they all access the same Container. Two of them access the same counter.

Running the program shows that the counters updated as expected.

$ javac Main.java
$ java Main
{a=20000, b=10000}

Next, we’ll look at implementing this same state management task using only threads and other synchronization constructs available in Java.