Immutability as a concurrency strategy
The cheapest concurrency fix there is: build values that cannot change, so no thread can break them.
Open this lesson in the learning hubKey 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 classfinal, and return a new object instead of mutating in place. - A
recordgives 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.
finalfields carry a special promise: ifthisnever 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.