cswork / libraryOpen practice workspace

Articles

Thread-Safe Singleton Interview Questions: Initialization, Publication, and Testability

Compare Java singleton initialization and publication, explain holder failures and class-loader scope, and preserve testability without resetting global state.

Thread-safe singleton interview questions are easy to answer incompletely. A candidate writes double-checked locking, adds volatile, and stops. The interviewer changes one requirement: initialization needs a retry, two tests need different clocks, or a plugin loads another copy of the class.

Start with the singleton's scope and lifetime, then separate unique construction, safe publication, and safe operations on its state. Eager construction or the initialization-on-demand holder can satisfy a simple Java requirement; neither automatically supplies retry semantics, distributed uniqueness, or test isolation.

This guide uses official Java specifications and original design exercises, not candidate reports about an employer's exact questions. The examples use ordinary Java 21-compatible syntax; they are documentation-reviewed, with no local JVM execution claimed.

What must “one instance” mean?

Ask whether the requirement is one object per defined class, one service per application container, one process-wide resource owner, or one coordinator across machines. Those are different boundaries. A static field cannot elect a leader for several application replicas.

The official JVM loading specification identifies a runtime class by its name and defining loader. Two separately defined copies can therefore have separate static fields. Two child loaders that both delegate to the same parent definition do not necessarily create two copies.

Use that distinction in an original plugin example. Plugin A and plugin B each define their own Registry class from identical bytes. Each class's static singleton can exist once while the process still contains two registries. If both plugins must share one registry, move ownership to an appropriate shared boundary and pass the dependency deliberately.

Then establish lifetime. Who closes files or network clients? Can configuration change? Does the first call provide parameters, and what happens if a later caller supplies different parameters? “First caller wins” may be a valid contract, but an accidental global configuration race is not a design.

Compare eager construction, a holder, and double-checked locking

For a simple object with fixed configuration, choose the smallest mechanism that meets the contract. The following comparison is our design guidance, grounded in the initialization and memory-model rules rather than a performance benchmark.

ApproachCreation pointUseful fitRequirement to examine
Static final instanceClass initializationCheap, fixed objectWhen the class is first actively used
Nested holderHolder initializationLazy, fixed objectFailure is tied to holder initialization
Synchronized accessorFirst successful accessor callSimple explicit factory logicLocking, retries, and constructor side effects
Volatile double-checked lockingFirst successful creation under lockJustified custom lazy lifecycleBoth checks, publication, and failure policy

“Eager” does not automatically mean “at JVM startup.” A static final instance is constructed when the owning class initializes. The official class initialization triggers include active uses such as invoking a declared static method or reading a nonconstant static field. Merely mentioning a class name is not a universal initialization trigger.

Do not justify double-checked locking by asserting it is always faster. First determine whether accessor overhead matters in a measured workload. A synchronized accessor can be a clearer answer when access is infrequent and the lifecycle needs explicit control.

Explain the holder idiom with the actual boundary

This is an original minimal holder implementation:

public final class Registry {
    private Registry() {}

    private static final class Holder {
        static final Registry INSTANCE = new Registry();
    }

    public static Registry instance() {
        return Holder.INSTANCE;
    }
}

Calling Registry.instance() initializes Registry if necessary; accessing Holder.INSTANCE initializes Holder. The official detailed initialization procedure coordinates competing initialization requests. That mechanism supplies the publication boundary for this pattern.

Draw two callers arriving before initialization finishes. One initializes the holder; the competing caller waits for that initialization to finish before obtaining the field. Your explanation should identify the initialized class, not hand-wave about a universal lock on “all singletons.”

Keep construction simple. A constructor that registers this with an external callback can expose an incomplete object through another route. A class-initialization mechanism does not make such an escape a sensible lifecycle. Likewise, recursive initialization dependencies can observe default state or create blocking cycles. Removing those dependencies is often clearer than adding another lock.

A good interview answer includes the negative boundary: the holder publishes this instance; it does not coordinate every later mutation inside it.

Write double-checked locking without deleting its proof

When custom lazy creation is justified, both the synchronization and the volatile reference matter:

public final class LazyRegistry {
    private static volatile LazyRegistry instance;
    private LazyRegistry() {}

    public static LazyRegistry instance() {
        LazyRegistry result = instance;
        if (result == null) {
            synchronized (LazyRegistry.class) {
                result = instance;
                if (result == null) {
                    result = new LazyRegistry();
                    instance = result;
                }
            }
        }
        return result;
    }
}

The first check allows an initialized reference to return without acquiring the monitor. The second check occurs after acquiring that monitor, because another caller may have initialized the instance while this caller waited. The local variable keeps the chosen reference explicit and avoids unnecessary repeated volatile reads on the fast path.

Official JLS happens-before rules supply the ordering relationship between a volatile publication and subsequent reads of that variable. Without that publication discipline, locking only writers does not establish the required relationship for readers that bypass the lock.

Do not describe this as one particular processor instruction being rearranged in every run. The point is what the Java contract guarantees. A stress test that happens to return the expected fields cannot replace that reasoning. The Java Memory Model guide develops the broader publication proof.

What happens when creation fails?

Compare two original scenarios. A holder initializer calls a factory that throws a runtime exception. A synchronized accessor calls the same factory while its instance field remains null. They need not have the same second-call behavior.

The JVM initialization procedure records unsuccessful class initialization as an erroneous state. With the runtime-exception scenario, the initiating caller can receive ExceptionInInitializerError; a later initialization attempt for that same defined class encounters NoClassDefFoundError. This is not an automatic retry of the original factory.

By contrast, an ordinary accessor that assigns its field only after successful construction can attempt construction again on a later call. That is an implementation choice, not evidence that unlimited retries are appropriate.

Requirement changeHolder implicationExplicit lifecycle implication
Temporary network failureFailed holder is not a retry loopDefine retry/backoff and caller errors
Initialization has side effectsPartial work may already existDefine cleanup or idempotent recovery
Several callers arrive during failureClass initialization governs failureDefine who owns the shared attempt
Service must be closed and reopenedStatic final lifetime is awkwardModel states and replacement explicitly

A factory that creates a remote resource and then fails locally can leave work behind. Repeating it may create another remote resource even though there was never a successful local singleton assignment. Distinguish “one published object” from “one attempted external side effect.”

For a recoverable service, consider explicit states such as uninitialized, starting, ready, failed, and closed. Decide whether callers share one in-flight future, whether a failure is cached, and who may initiate a new generation. That discussion may show that a lifecycle manager is the better abstraction.

Why can a safely created singleton still lose updates?

Suppose the one shared instance owns an integer completed, and two callers execute completed++. Unique construction does not make that expression atomic. An original scheduled trace starts at 8: both callers read 8, both calculate 9, and both write 9. Two attempted increments produce one net increase.

Choose protection for the invariant. An independent counter can use an AtomicInteger increment operation. A rule involving balance and reservations may require one lock around a complete decision or replacement of an immutable state value.

Also define ownership of returned objects. A synchronized getter returning a mutable list can release the lock before the caller modifies that list. The lock then does not protect the caller's mutation. Returning a snapshot, exposing controlled operations, or documenting a separate ownership contract may be necessary.

Avoid using a singleton as a dumping ground for request-specific state. Two requests with different users should not overwrite a shared currentUser field. Making that field volatile improves reference visibility; it does not assign each request its own context.

How do you preserve testability without a global reset?

Our recommendation is to let an application composition boundary choose a shared production instance while clients depend on an interface. Tests can then supply independent substitutes.

import java.time.Instant;

interface TimeSource {
    Instant now();
}

final class ReceiptService {
    private final TimeSource time;
    ReceiptService(TimeSource time) {
        this.time = java.util.Objects.requireNonNull(time);
    }
    Instant issuedAt() {
        return time.now();
    }
}

Production wiring can pass the same time provider to every receipt service. Test A can pass a provider returning midnight; test B can pass one returning noon. Both tests can run concurrently without resetting a static clock or depending on test execution order.

That is still compatible with a deliberately shared production dependency. It separates how many objects the application constructs from whether every client reaches into a global accessor. It also makes construction dependencies visible rather than hiding them inside an innocent-looking method.

If you propose a reset-for-tests method, explain who can call it while other threads hold references. Resetting the field does not revoke those old references, and a later caller can receive a new object. The advertised one-instance lifetime has changed. An injected dependency usually expresses the test requirement more clearly.

What about enum singletons, serialization, and reflection?

An enum with one constant can be a concise representation of a fixed singleton. The official enum specification describes special restrictions on enum instantiation and identity, including serialization behavior. The Constructor API also specifies that reflective construction rejects enum types.

Those properties do not make enum members' mutable fields thread-safe, and they do not make one constant global across independent defining loaders. Nor do they solve a service's need for configurable construction, retries, or replacement.

For an ordinary serializable singleton class, deserialization identity deserves a deliberate policy rather than an assumption that the private constructor is enough. Similarly, ordinary private constructors should not be described as a complete security boundary against all reflective or privileged access. Keep the discussion scoped to supported application behavior and the relevant runtime access rules.

Practise the changed requirement, not only the pattern

QuestionRehearsal focus
Explain Runnable vs Callable and thread-safe SingletonExplain both lazy checks and safe publication
Explain GC, singleton, and OOP principlesConnect lifetime to object ownership
Explain Spring Dependency Injection and Choose an Injection StyleMake collaborators replaceable
Implement and compare key design patternsChoose a pattern from requirements
Explain Java concurrency and fix i++ raceSeparate publication from atomic operations

Rehearse one initial design, then ask for lazy construction, temporary failure, two plugin loaders, and parallel tests. For each change, name the property that broke before editing code. Use the Singleton question for the implementation answer and the dependency-injection question for a design follow-up. A convincing answer explains when a shared instance is useful and who controls its lifetime.

Sources and Further Reading

Research checked October 4, 2026. Specification-backed rules are separated from original design recommendations; snippets are documentation-reviewed and are not a JVM benchmark.