When is a companion object initialized, and what concurrency/initialization pitfalls arise from putting mutable state in it?
answer
- Companion is a lazily, thread-safely initialized singleton
- init/property initializers run once on first use
- var in companion = global shared state
- Guard with @Synchronized / Atomic / ConcurrentHashMap / Mutex
- Prefer val / const val / by lazy
basics
~20 sThe companion is created once, lazily, the first time the class or a companion member is used. Because it is shared by everything, any mutable state in it is global and must be made thread-safe.
solid answer
~40 sA companion object is a singleton initialized lazily and thread-safely on first access — the first time the enclosing class is referenced in a way that triggers its loading, or a companion member is touched. Its 'init' block and property initializers run once at that point. Because there is exactly one companion instance per class for the whole JVM, any 'var' or mutable container in it is effectively global shared state: concurrent access needs synchronization (e.g. @Synchronized, java.util.concurrent.atomic types, or a Mutex), and lazy fields should use 'by lazy' (thread-safe by default). Pitfalls include hidden initialization order coupling, accidental memory retention (the companion never gets GC'd while the class is loaded), and test pollution from static mutable state shared across tests. Prefer immutable 'val'/'const val' or 'by lazy' over raw mutable vars.
code
kotlin · 6 linesclass IdGen {
companion object {
private val counter = java.util.concurrent.atomic.AtomicLong(0)
fun next(): Long = counter.incrementAndGet() // thread-safe
}
}go deeper
Knows the companion is created once and members are class-level.
Understands lazy, one-time, thread-safe initialization and that init runs once.
Identifies companion var as global state and applies Atomic/Synchronized/by lazy correctly.
Weighs lifecycle, memory retention, test isolation, and init-order coupling in design and reviews.
## When it initializes A companion object is a **singleton** that is created **lazily** the first time it is needed — typically on first access to the enclosing class or any companion member. The JVM's class-loading and Kotlin's generated initializer make this **thread-safe** (the runtime guarantees the companion's static init runs once). Its property initializers and any `init { }` block run exactly once at that moment. ```kotlin class Counter { companion object { init { println("companion init") } // runs once, on first use var total = 0 // shared mutable state! } } ``` ## Why mutable state is risky There is **one** companion instance per class for the whole program, so a companion `var` is **global state**: - **Concurrency:** multiple threads mutating `total` race. Guard it with `@Synchronized` functions, `java.util.concurrent.atomic.AtomicInteger`/`AtomicReference`, a `kotlinx.coroutines.sync.Mutex`, or `ConcurrentHashMap` for maps. - **Lazy, thread-safe init:** for an expensive value, prefer `val instance by lazy { ... }`. `by lazy` uses `LazyThreadSafetyMode.SYNCHRONIZED` by default, computing once even under contention. - **Test pollution:** static mutable state persists across tests in the same JVM; one test can leak into another. Reset in setup or avoid mutable companion state. - **Memory retention:** the companion (and what it references) lives as long as the class is loaded — accidental caches there can pin large objects. - **Initialization-order coupling:** if a companion references another class's companion during init, you can hit subtle ordering bugs or partially-initialized reads. ## Safer patterns ```kotlin class Registry { companion object { // immutable / inlined const val MAX = 1000 // computed once, thread-safe val shared by lazy { buildExpensiveThing() } // concurrent-safe mutable container private val seen = java.util.concurrent.ConcurrentHashMap<String, Int>() @Synchronized fun record(k: String) { seen.merge(k, 1, Int::plus) } } } ``` ## Key APIs/keywords to name - `by lazy` (default `SYNCHRONIZED` mode), `@Synchronized`, `Atomic*` types, `ConcurrentHashMap`, `Mutex`/`withLock`, `init { }`. ## Bottom line Treat companion state as **global**: keep it immutable (`val`/`const val`) or behind `by lazy`; if it must be mutable, make access explicitly thread-safe and beware test/lifecycle leakage.
- Is a companion object's initialization thread-safe?Yes. The runtime guarantees the companion's static initialization runs once, even under concurrent first access.
- What is the default thread-safety mode of 'by lazy'?LazyThreadSafetyMode.SYNCHRONIZED, so the initializer runs at most once even with concurrent access.
- Why can companion mutable state cause flaky tests?It is static and survives across tests in the same JVM, so state leaks between tests unless reset.
A companion var is like a single whiteboard the whole building shares — handy, but everyone scribbling at once causes chaos unless you take turns.
saying these in an interview costs you the question
- Claiming the companion is eagerly initialized at JVM startup
- Saying companion init is not thread-safe
- Putting unsynchronized var state in a companion without noticing it is global
- Thinking each instance of the class gets its own companion
- Using by lazy and assuming it is never thread-safe