Java
critical sections
synchronization
threading
concurrent programming

In Java critical sections, what should I synchronize on?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

In Java, the object you synchronize on is the lock that protects a critical section. The correct lock is the one shared by every thread that touches the same mutable state. Most synchronization bugs happen because the code locks on the wrong object, exposes the lock publicly, or uses multiple unrelated locks to guard the same data.

Synchronize on the Object That Guards the Shared State

A critical section exists because multiple threads can access the same mutable data. The lock must be tied to that shared data, not chosen arbitrarily.

Simple example:

java
1public class Counter {
2    private int value = 0;
3
4    public synchronized void increment() {
5        value++;
6    }
7
8    public synchronized int get() {
9        return value;
10    }
11}

Here the monitor is this, and it works because the protected state belongs to the instance itself.

Prefer a Private Lock Object for Internal State

Synchronizing on this is common, but a private lock object is often safer because external code cannot accidentally lock on it.

java
1public class Counter {
2    private final Object lock = new Object();
3    private int value = 0;
4
5    public void increment() {
6        synchronized (lock) {
7            value++;
8        }
9    }
10
11    public int get() {
12        synchronized (lock) {
13            return value;
14        }
15    }
16}

This is a strong default for classes whose locking policy should remain entirely internal.

Do Not Synchronize on Publicly Accessible Objects

Avoid synchronizing on objects that other code can also lock:

  • string literals
  • boxed primitives
  • publicly exposed collections
  • objects returned from getters

Bad example:

java
1private final String lock = "LOCK";
2
3public void doWork() {
4    synchronized (lock) {
5        // unsafe choice
6    }
7}

String literals are interned, so unrelated code may accidentally share the same lock. That can create surprising contention or deadlocks.

Keep One Lock Per Protected State Group

If two fields must change together to remain consistent, they should usually be protected by the same lock.

java
1public class Account {
2    private final Object lock = new Object();
3    private int balance;
4    private int version;
5
6    public void deposit(int amount) {
7        synchronized (lock) {
8            balance += amount;
9            version++;
10        }
11    }
12}

Using separate locks for data that must remain consistent together can create race conditions even though each field is individually synchronized.

Synchronize the Smallest Useful Critical Section

Lock only the code that truly needs exclusive access. The longer the critical section, the more threads block each other.

java
1public void process(List<String> items) {
2    String snapshot;
3    synchronized (lock) {
4        snapshot = String.join(",", items);
5    }
6
7    System.out.println(snapshot);
8}

Here the expensive or slow external action happens outside the lock. That reduces contention.

Consider Higher-Level Concurrency Tools

Not every critical section should use synchronized. Java’s concurrent utilities are often better:

  • 'ReentrantLock when you need advanced lock control'
  • 'ReadWriteLock for read-heavy workloads'
  • 'AtomicInteger for simple counters'
  • concurrent collections such as ConcurrentHashMap

Example with AtomicInteger:

java
1import java.util.concurrent.atomic.AtomicInteger;
2
3public class Counter {
4    private final AtomicInteger value = new AtomicInteger();
5
6    public void increment() {
7        value.incrementAndGet();
8    }
9
10    public int get() {
11        return value.get();
12    }
13}

This avoids explicit monitor locking for a simple counter.

Do Not Lock on Objects You Do Not Own

If a collection or dependency is passed in from outside, synchronizing on it can be dangerous because other code may use the same object for unrelated locking.

Safer pattern:

java
1public class SafeWrapper {
2    private final Object lock = new Object();
3    private final List<String> items;
4
5    public SafeWrapper(List<String> items) {
6        this.items = items;
7    }
8
9    public void add(String item) {
10        synchronized (lock) {
11            items.add(item);
12        }
13    }
14}

Owning the lock object makes the concurrency policy clearer.

Common Pitfalls

The most common mistake is using different locks to protect the same state. Threads then appear synchronized, but they are not actually coordinating with each other.

Another issue is synchronizing on this in a type that is widely exposed, which lets outside code interfere with internal locking.

Developers also often hold locks while doing slow I/O or remote calls. That can create unnecessary contention and even deadlocks.

Summary

  • Synchronize on the lock that truly guards the shared mutable state.
  • A private final lock object is usually the safest default.
  • Avoid locking on public, shared, or interned objects such as string literals.
  • Keep related state under the same lock if consistency depends on it.
  • Prefer higher-level concurrency utilities when they express the problem more clearly than raw synchronized.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.