Locking On A Reassignable Field

ID

java.no_synchronize_on_mutable_field

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

Java

Tags

concurrency, reliability

Description

Reports a synchronized block whose monitor is a field that is neither final nor left alone — the field can be, and somewhere in the same file is, assigned a different object.

Both conditions must hold. A non-final lock field that is never reassigned is not reported: that is a missing keyword, not a concurrency bug.

Rationale

A monitor is an object, not a variable. synchronized (rosterLock) locks whatever instance rosterLock happens to hold at the moment the block is entered — so reassigning the field hands the next thread a different monitor:

class SessionRegistry {
  private Object rosterLock = new Object();   // not final
  private int active;

  void admit() {
    synchronized (rosterLock) {               // locks the object held right now
      active++;
    }
  }

  void resetRoster() {
    rosterLock = new Object();                // the next caller locks something else
  }
}

Once resetRoster runs, a thread entering admit acquires the new object without contest and executes the critical section side by side with a thread still holding the old one. The block compiles, reads as synchronized, and excludes nobody. This is the worst shape a concurrency defect can take, because the code looks correct exactly where it fails.

The rule stays silent on four shapes:

  • synchronized (this) and synchronized (Outer.this), which have their own rule. The two can never both fire on one block — this one only proceeds when the monitor is a plain field reference.

  • Locals and parameters. Each invocation evaluates them afresh, so concurrent callers lock different objects for a different reason.

  • A monitor reached through another object, such as other.lock — there is no basis to decide who owns that lock.

  • Anything the symbol table cannot attribute to a declaration. An unresolved name is not evidence of a defect, and guessing would report ordinary locks on inherited fields.

Remediation

Make the lock field final. The reference can then never change, so every thread contends for the same monitor:

class SessionRegistry {
  private final Object rosterLock = new Object();
  private int active;

  void admit() {
    synchronized (rosterLock) {
      active++;
    }
  }

  void resetRoster() {
    synchronized (rosterLock) {   // reset the state, not the lock
      active = 0;
    }
  }
}

If the reassignment was the point — swapping the guarded state wholesale — guard the swap with the same fixed lock instead of replacing the lock itself. Where the critical section needs timed or interruptible acquisition, a final java.util.concurrent.locks.ReentrantLock serves the same purpose.

© 2026 Xygeni. All rights reserved