Back to Synchronization
Q7Question 7 of 8

Why is synchronizing on a String literal a bug?

Key Concepts
  • String literals are interned: the JVM stores one instance per unique literal in the String pool, shared across the entire JVM.
  • Two unrelated classes synchronizing on the same string literal lock on the same object, causing unexpected blocking.
  • The same problem applies to Boolean.TRUE, Boolean.FALSE, and Integer values in the cached range (-128 to 127).
  • The fix is private final Object lock = new Object(): private (no external access), final (no reassignment), unique (not shared).
  • synchronized(this) is also risky because external code holding a reference to your object can lock on it.
  • SonarQube and IntelliJ flag synchronization on interned objects as a critical bug.

Definition

String literals are interned and shared across the entire JVM, so synchronizing on a String literal accidentally creates a global lock that blocks unrelated code.
  • String interning – the JVM maintains a String pool with one instance per unique literal. "LOCK" in Class A and "LOCK" in Class B are the exact same object in memory.
  • Accidental global lock – if two unrelated classes synchronize on "LOCK", they lock on the same object. One class's critical section blocks the other, even though they have no relationship.
  • Cached objects – Boolean.TRUE, Boolean.FALSE, and Integer.valueOf(n) for n in the range -128 to 127 are also shared singleton instances. Synchronizing on them has the same problem.
  • Fix – always use private final Object lock = new Object(). Private: no external code can access it. Final: it cannot be reassigned. new Object(): unique to your class, not shared with anyone.

Synchronizing on a String literal is like using the front door key of the building as your vault key. Every shop in the building has the same front door key. When you lock your vault with this key, you accidentally lock the entire building. Two shops trying to lock at the same time deadlock each other. The fix is to give each shop its own unique key that nobody else possesses.

Understanding the Concepts

Why is synchronizing on a String literal a bug?

String literals are interned by the JVM. Two classes using "ACCOUNT_LOCK" as a lock object are synchronizing on the same instance in memory.

JAVA
package com.bank.service;

public class AccountService {
    public void debit() {
        synchronized ("ACCOUNT_LOCK") { // interned String
            // process debit
        }
    }
}
JAVA
package com.bank.report;

public class ReportService {
    public void generateReport() {
        synchronized ("ACCOUNT_LOCK") { // same interned String!
            // takes 30 seconds to generate report
        }
    }
}

While the report generates (30 seconds), ALL account debits are BLOCKED. These classes do not even know about each other.

How can you prove that string literals are the same object?

JAVA
package com.bank.core;

public class StringInterningProof {
    public static void main(String[] args) {
        String a = "LOCK";
        String b = "LOCK";
        System.out.println("Same object? " + (a == b)); // true

        String c = new String("LOCK");
        System.out.println("Same object? " + (a == c)); // false (new instance)

        String d = c.intern();
        System.out.println("Same object? " + (a == d)); // true (interned)
    }
}
TEXT
Same object? true
Same object? false
Same object? true

What other objects have this problem?

Boolean.TRUE, Boolean.FALSE, and cached Integer values (-128 to 127) are shared singletons.

JAVA
package com.bank.core;

public class CachedObjectBugs {
    public void method1() {
        synchronized (Boolean.TRUE) { } // shared globally!
    }

    public void method2() {
        synchronized (Integer.valueOf(1)) { } // cached, shared!
    }

    // Both methods above use globally shared lock objects.
    // Any code in the JVM using the same object is blocked.
}

What is the correct fix?

Use a private final Object as the lock. It is unique to your class and cannot be accessed or reassigned.

JAVA
package com.bank.service;

public class SafeAccountService {
    private final Object lock = new Object();

    public void debit(java.math.BigDecimal amount) {
        synchronized (lock) {
            // Private: no external code can lock on this
            // Final: cannot be reassigned to a different object
            // new Object(): unique, not shared with anyone
        }
    }
}

What other anti-patterns should you avoid?

JAVA
package com.bank.service;

public class LockAntiPatterns {
    // WRONG: synchronized(this) exposes the lock
    public synchronized void riskyMethod() {
        // External code can do: synchronized(myService) { sleep(60000); }
        // Blocks ALL your synchronized methods for 60 seconds!
    }

    // WRONG: non-final lock can be reassigned
    private Object lock = new Object(); // NOT final!
    public void fragileMethod() {
        synchronized (lock) {
            lock = new Object(); // reassigned! Next caller locks on a different object!
        }
    }

    // WRONG: new Object() inside synchronized
    public void brokenMethod() {
        synchronized (new Object()) { // new lock every call: no synchronization!
        }
    }
}

How do IDEs and tools help?

IntelliJ IDEA warns: "Synchronization on a non-final field" and "Synchronization on interned String". SonarQube flags synchronized("...") as a critical bug (rule S1860). These checks catch the problem at development time.

What should a senior engineer know?

  • String.intern() memory: in Java 7+, interned strings are stored in the heap (not PermGen), so they are garbage-collected when unreferenced. But string literals are always reachable from the constant pool.
  • Lock object identity: the lock's identity (memory address) is what matters, not its value. Two different new Object() instances are different locks even though they have the same toString() output.
  • Kotlin val: in Kotlin, val lock = Any() is already final. The language prevents the reassignment bug by default.

Visual Overview

DIAGRAM
  String Pool (JVM-wide):
  ┌──────────────────────────────────────┐
  │  "ACCOUNT_LOCK" ──▶ Object@0x7f3a    │
  │  "REPORT_LOCK"  ──▶ Object@0x7f3b    │
  │  "LOCK"         ──▶ Object@0x7f3c    │
  └──────────────────────────────────────┘

  AccountService: synchronized("ACCOUNT_LOCK") ──▶ locks Object@0x7f3a
  ReportService:  synchronized("ACCOUNT_LOCK") ──▶ locks Object@0x7f3a (SAME!)
  Both classes block each other!

  Fix: private final Object lock = new Object();
  AccountService.lock ──▶ Object@0xaaaa (unique)
  ReportService.lock  ──▶ Object@0xbbbb (unique)
  Different objects: no blocking!

Key Takeaway

Synchronizing on a String literal is a bug because String literals are interned: the JVM stores one instance per unique literal in the String pool, shared across the entire JVM. If two unrelated classes synchronize on "LOCK", they lock on the exact same object, causing unexpected blocking and potential deadlock between code that has no relationship.

The same problem applies to Boolean.TRUE, Boolean.FALSE, and cached Integer values in the range -128 to 127. All are shared singleton instances.

The fix is always private final Object lock = new Object(). Private prevents external access. Final prevents reassignment. new Object() creates a unique instance. This three-word pattern is the correct foundation for every lock in your code.

Remember one sentence: never synchronize on interned objects like String literals or cached Integers; always use a private final Object as the lock.

---

Share
Use ← → arrow keys to navigate