Card 03/ 07

GotchaDifficulty: Intermediate1 min

Mutable State in a Singleton Bean

A counter that looks private to each caller is shared by everyone, and under load it also races.

A singleton bean is shared by every caller, so any field it changes is changed for everyone. A counter or a list kept in a singleton's field looks private to each caller in the code, and it is not.

RideCounter looks as if it gives each caller a running total of their own. There is exactly one RideCounter, and every caller shares it:

java
class RideCounter {
    private int total = 0;
    int recordRide() { return ++total; }
}

// two unrelated callers, nowhere near each other in the codebase
RideCounter forRequestA = context.getBean(RideCounter.class);
System.out.println("request A records ride #" + forRequestA.recordRide());

RideCounter forRequestB = context.getBean(RideCounter.class);
System.out.println("request B records ride #" + forRequestB.recordRide());
text
request A records ride #1
request B records ride #2
run in a container: one instance, one running total

Request B did not get a counter starting at zero; it got request A's counter, already at one. That is not a bug in the container, because it is exactly what a singleton promises. The bug is treating a shared, changing field as if each caller had a private copy. Under real concurrent traffic it gets worse: two threads (separate lines of execution, each handling a request) incrementing the same field at once can lose updates, which is called a race condition.

Keep a singleton's fields unchanged after startup, and move changing data somewhere scoped to whoever is asking: a local variable, a method parameter, or a bean with a shorter scope (a scope decides how long one instance of a bean lives and who shares it). If a count really must be shared, use a thread-safe type such as AtomicInteger.