Immutability as a concurrency strategy

Multithreading · lesson 19 of 38 · 3 min read

The cheapest concurrency fix there is: build values that cannot change, so no thread can break them.

Open this lesson in the learning hub

Key points

  • Data that never changes cannot race. No locks, no visibility rules, no lost updates, nothing to reason about.
  • Make the fields final, make the class final, and return a new object instead of mutating in place.
  • A record gives you final fields, a constructor, equals and toString for free. It is the default shape for a value.
  • Copy mutable inputs in the constructor and hand out unmodifiable views, or callers can still reach inside your object.
  • final fields carry a special promise: if this never escapes the constructor, every thread sees them fully built.
  • Copying costs a little memory and removes a whole class of bugs. Start immutable, and add mutation only when you measure a need.

Example

import java.util.ArrayList;
import java.util.List;

public class Main {
    record Money(String currency, long amount) {           // final fields, no setters
        Money plus(long extra) { return new Money(currency, amount + extra); }
    }

    static final class Config {
        private final List<String> hosts;
        Config(List<String> hosts) { this.hosts = List.copyOf(hosts); }   // defensive copy
        List<String> hosts() { return hosts; }
    }

    public static void main(String[] args) throws InterruptedException {
        Money price = new Money("USD", 10);
        List<String> mutable = new ArrayList<>(List.of("a", "b"));
        Config config = new Config(mutable);
        mutable.add("c");                                  // the copy inside Config is unaffected

        Thread[] ts = new Thread[3];
        for (int i = 0; i < ts.length; i++) {
            ts[i] = new Thread(() -> {
                Money local = price.plus(5);               // returns a new object, shared one intact
                System.out.println(Thread.currentThread().getName() + " sees " + price + " -> " + local);
            }, "reader-" + i);
            ts[i].start();
        }
        for (Thread t : ts) t.join();

        System.out.println("shared unchanged : " + price);
        System.out.println("config hosts     : " + config.hosts());
        try { config.hosts().add("d"); }
        catch (UnsupportedOperationException e) { System.out.println("read-only copy    : no lock needed"); }
    }
}

If it cannot change, it cannot be a race condition.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Multithreading course, and every lesson in it is listed on the Multithreading contents page.