Why is choosing the lock object carefully important, and what are the consequences of synchronizing on a shared or publicly accessible object versus a private one?
answer
- Lock object identity = the locking policy
- Private final Object lock = new Object() — the idiom
- Never lock on this if you want encapsulation; callers can grab it
- Never lock on interned Strings or boxed Integers (shared caches)
- Lock field must be final; never lock a reassignable/mutable reference
basics
~20 sThe lock object determines what is mutually exclusive. If you lock on something other code can also see and lock (like this or a String/Integer), outside code can interfere, cause unexpected contention, or deadlock. Locking on a private final object you fully control keeps the lock policy safe and encapsulated.
solid answer
~50 sA synchronized region only excludes other regions that lock the *same* object, so the lock object IS your locking policy. Two failure modes come from a poor choice. First, locking on a publicly reachable object — this, a shared constant, an interned String, or a boxed Integer from a cache — lets unrelated code acquire the same monitor, causing surprise contention or even deadlock, and breaks encapsulation: callers can hold your lock. Second, locking on the wrong granularity (different access paths using different locks) leaves races. The standard practice is the *private lock object* idiom: a private final Object guard = new Object(); synchronized on guard. Because nothing else can reference it, only your class's code can acquire it, so the lock policy is fully encapsulated and provable. Reserve this/Class locks for cases where you intentionally expose the lock as part of the contract. Never lock on mutable fields, boxed primitives, or interned strings.
code
java · 13 linespublic final class Account {
private final Object lock = new Object(); // private, final, never escapes
private long balance;
public void deposit(long amount) {
synchronized (lock) { balance += amount; }
}
public long balance() {
synchronized (lock) { return balance; }
}
// Avoid: synchronized("LOCK"), synchronized(Integer.valueOf(1)),
// or locking a reassignable field -- all share or shift the monitor.
}go deeper
Understands that you must lock on some object and that locking on the same object is what serializes code.
Knows not to lock on Strings/boxed primitives and can use a private lock field, even if the rationale is partial.
Applies the private-final-lock idiom deliberately, explains encapsulation/interference/deadlock consequences, and audits that every access path holds the one guarding lock.
Sets a codebase-wide locking-policy convention (guarded-by docs, final lock fields, lock ordering), reviews APIs that intentionally expose a lock as contract, and weighs lock splitting/striping for scalability against analyzability.
## The lock object is the policy `synchronized(obj)` excludes only other regions that synchronize on the **same** `obj`. So the *identity* of the lock object defines exactly which code paths are serialized. Choosing it is a design decision, not a detail. ## Failure mode 1: locking on a publicly reachable object If the object you lock on is visible outside your class, **anyone** can acquire its monitor: - **`synchronized(this)` / synchronized methods.** `this` is handed to every caller. A caller (or a subclass, or framework code) can write `synchronized(yourObject){...}` and now holds *your* lock — introducing contention you never anticipated or deadlock against your internal locking. Your locking policy leaks out of the class. - **Interned strings.** `synchronized("LOCK")` is dangerous: string literals are *interned*, so the same `String` instance is shared JVM-wide. Two completely unrelated classes that both lock on the literal `"LOCK"` share one monitor — accidental global contention/deadlock. - **Boxed primitives.** `Integer i = 1; synchronized(i){}` — small `Integer`/`Boolean` values come from a shared cache (autoboxing reuses `Integer.valueOf` instances for -128..127), so different code locking on `1` shares a monitor. Same hazard. - **Shared constants / singletons** reachable by others have the same problem. ## Failure mode 2: wrong granularity Even a private lock is wrong if **not all** access paths to the guarded state use it. If method X locks `guardA` and method Y locks `guardB` but both touch the same field, they don't exclude each other → race. Rule: each piece of mutable state is *guarded by* exactly one lock, and **every** access holds it. ## The private lock object idiom ```java public class Account { private final Object lock = new Object(); // private, final, never escapes private long balance; public void deposit(long n) { synchronized (lock) { balance += n; } } public long balance() { synchronized (lock) { return balance; } } } ``` Because `lock` is `private` and never returned or passed out, **no external code can ever acquire it**. The locking policy is fully *encapsulated*: you can reason about every site that holds it just by reading this class. `final` ensures the reference can't be reassigned (which would split the lock). This is the recommended default for new code. ## When `this`/Class locks are acceptable Using `this` (a synchronized method) is fine when: - the class is final or the locking is part of a documented, intended contract (e.g. clients are *meant* to lock the instance to compose operations), and - you accept that subclasses and callers can participate in the lock. Many JDK classes historically used `this` for this reason. But for fresh, encapsulated code, a private lock avoids the whole class of surprises. ## Mutability trap Never lock on a **mutable field** whose reference can change: ```java synchronized (this.list) { ... } // if this.list is reassigned, threads lock different objects! ``` If the field is reassigned, different threads synchronize on different objects and exclusion breaks. Lock objects should be `final`. ## Consequences summary - Public lock object → external interference, surprise contention, deadlock, broken encapsulation. - Shared cached object (interned String, boxed Integer) → accidental cross-class shared lock. - Mutable/non-final lock field → exclusion silently breaks on reassignment. - Inconsistent locks across access paths → races. - Private final dedicated lock + every access holding it → safe, encapsulated, analyzable.
- Why is synchronizing on a String literal or a boxed Integer dangerous?Both come from shared caches — interned string literals and the small-Integer cache reuse the same instance JVM-wide. Unrelated code locking on the 'same' literal value shares one monitor, causing accidental contention or deadlock across classes that know nothing about each other.
- What's the risk of declaring the lock field non-final, e.g. private Object lock?If the reference is reassigned, threads after the reassignment lock a different object than threads before it, so mutual exclusion silently breaks. The lock field should be final.
saying these in an interview costs you the question
- Synchronizing on an interned String literal or a boxed Integer/Boolean (shared cached instances).
- Locking on this and assuming external code can never interfere with your lock.
- Locking on a mutable/reassignable field reference so different threads end up on different objects.
- Thinking any private object works regardless of whether ALL access paths use that same lock.
- Believing the choice of lock object is irrelevant as long as 'something' is synchronized.