Explain the three `LazyThreadSafetyMode` values (SYNCHRONIZED, PUBLICATION, NONE) and when you'd pick each.
answer
- SYNCHRONIZED = double-checked locking, runs once, default
- PUBLICATION = CAS, first result wins, may run many times
- NONE = no sync, single-thread only, fastest
- Exceptions are not cached (retry on next access)
- Also a `lazy(lock) { }` overload
basics
~20 sThey control how a lazy value behaves with many threads. SYNCHRONIZED locks so only one thread computes it (the safe default). PUBLICATION may compute in several threads but keeps the first result. NONE has no safety and is for single-thread use only.
solid answer
~50 s`lazy()` takes an optional `LazyThreadSafetyMode`: - **SYNCHRONIZED** (default): uses a lock (double-checked locking on the delegate) so exactly one thread runs the initializer; other threads block until it's done. Safe everywhere. - **PUBLICATION**: multiple threads may run the initializer concurrently, but the value is set atomically via a CAS — the **first** successfully computed result wins and is published to all; the losing results are discarded. Use when the computation is cheap/idempotent and you'd rather not hold a lock. - **NONE**: no synchronization at all — fastest, but if accessed from multiple threads behavior is undefined (could compute twice, see stale state). Only correct when access is confined to one thread (e.g. a UI thread) or otherwise externally synchronized. Pick SYNCHRONIZED by default; NONE for hot single-threaded paths; PUBLICATION rarely, for idempotent values where you want to avoid blocking.
code
kotlin · 10 linesimport kotlin.LazyThreadSafetyMode.*
// Default: only one thread ever runs the block
val safe by lazy(SYNCHRONIZED) { connectToDb() }
// Idempotent; several threads may compute, first wins
val idempotent by lazy(PUBLICATION) { System.currentTimeMillis() }
// UI-thread-only; cheapest, no guarantees if shared
val uiOnly by lazy(NONE) { inflateView() }go deeper
Knows the default is thread-safe and that there are other modes, even if hazy on the exact guarantees.
Correctly distinguishes all three modes and picks SYNCHRONIZED as the safe default, NONE for single-thread.
Explains double-checked locking, CAS/safe-publication for PUBLICATION, and the idempotency requirement; ties choice to profiling.
Reasons about memory-model guarantees (visibility, safe publication), library-API defaults, and the cost/benefit of relaxing from SYNCHRONIZED at scale.
## The factory overloads ```kotlin val a by lazy { compute() } // SYNCHRONIZED (default) val b by lazy(LazyThreadSafetyMode.PUBLICATION) { compute() } val c by lazy(LazyThreadSafetyMode.NONE) { compute() } val d by lazy(myLock) { compute() } // lock object overload, uses SYNCHRONIZED on `myLock` ``` `lazy()` returns different internal `Lazy<T>` implementations depending on the mode. ## SYNCHRONIZED (the default) Internally `SynchronizedLazyImpl` uses **double-checked locking**: it reads a volatile `_value` field; if unset, it synchronizes on a lock object and checks again, runs the initializer, and stores the result. Guarantees: - The initializer runs **at most once**. - Safe under concurrent access from any number of threads. - Cost: a `synchronized` block on the first contended access and a volatile read thereafter. An exception thrown by the initializer is **not** cached — the next access re-runs it. ## PUBLICATION `SafePublicationLazyImpl` allows **several threads to run the initializer simultaneously**, then uses an atomic compare-and-set (`AtomicReferenceFieldUpdater.compareAndSet`) so the **first** completed value is the one stored and observed by everyone; other computed values are thrown away. Use it when: - The initializer is **side-effect-free and idempotent** (running it twice is harmless), and - You want to avoid the blocking/lock of SYNCHRONIZED. It provides **safe publication** (the value is visible to all threads via the atomic write) but does **not** guarantee single execution. ## NONE `UnsafeLazyImpl` does plain non-volatile reads/writes — **no synchronization**. It's the fastest, but if two threads race you can get two initializer runs, or one thread can observe a partially constructed/stale value. Only use when access is provably confined to one thread (Android UI thread, single-threaded coroutine dispatcher) or guarded by your own synchronization. ## Choosing | Mode | Runs once? | Thread-safe? | When | |------|-----------|--------------|------| | SYNCHRONIZED | Yes | Yes | Default; unknown threading | | PUBLICATION | No (maybe many) | Yes (safe publish) | Cheap, idempotent, avoid lock | | NONE | Yes if single-thread | No | Confirmed single-thread hot path | Rule of thumb: keep the default unless profiling shows the lock/volatile cost matters.
- With PUBLICATION, is the initializer guaranteed to run only once?No. Multiple threads may run it concurrently; only the first completed result is published and kept. That's why it must be idempotent/side-effect-free.
- What happens if you use NONE and two threads access the property at the same time?Behavior is undefined: the initializer can run more than once, and a thread may observe a stale or partially-initialized value because there is no memory barrier.
saying these in an interview costs you the question
- Says PUBLICATION guarantees single execution
- Calls NONE 'safe but a bit slower' instead of unsafe
- Thinks SYNCHRONIZED locks on every read forever (it's double-checked)
- Cannot name the default mode (SYNCHRONIZED)
- Recommends NONE for shared state to 'improve performance'