Back to Blog
Java

Java Synchronized Method: Locking Behavior and Use Cases

java synchronized method: Understand how Java synchronized methods acquire locks, their reentrancy, performance tradeoffs, and when to prefer blocks or explicit locks.

thread safetyintrinsic locksconcurrencymonitorsynchronization
Illustration of a Java synchronized method locking an object to control thread access

A Java synchronized method is the simplest way to protect shared state from concurrent access. When you declare a method as synchronized, the JVM requires a thread to acquire the intrinsic lock (also called the monitor) before entering the method, and releases it automatically when the method exits, even if an exception is thrown. This guarantees that two threads cannot execute the same synchronized method on the same object concurrently, but it does not protect against all concurrency issues by itself.

Consider a counter class that multiple threads increment:

public class Counter { private int count; public synchronized void increment() { count++; } public synchronized int getCount() { return count; } }

Here both increment and getCount are synchronized on the same lock, so a read cannot observe a partially updated value. The lock is the Counter instance itself. If you omit synchronized on getCount, a reader could see a stale value because the write in increment is not guaranteed to be visible without a happens-before relationship.

How a Synchronized Method Acquires the Lock

The lock for an instance method is the object on which the method is called. When thread A calls counter.increment(), it must own the monitor associated with that counter object. If thread B already holds the monitor, thread A blocks until the monitor is released. The JVM inserts monitorenter and monitorexit bytecode instructions around the method body, but you do not see them in source code.

Because the lock is automatically released on both normal completion and exceptional exit, you avoid the risk of forgetting to unlock in a finally block, which is a common error when using explicit locks.

Instance Methods and Static Methods Lock Differently

A static synchronized method locks on the Class object representing the class, not on any instance. This is a crucial distinction. If you have a class with both static and instance synchronized methods, they use different locks and can run concurrently.

public class SharedRegistry { private static Map<String, String> cache = new HashMap<>(); public static synchronized void put(String key, String value) { cache.put(key, value); } public synchronized void update(String key, String value) { // This locks on the instance, not on SharedRegistry.class // It does not block static synchronized methods } }

If you need to protect static state, use a static synchronized method or synchronize on ClassName.class inside a block. Mixing instance and static synchronization on the same data can lead to race conditions because the locks are independent.

Reentrant Locking and Nested Calls

Intrinsic locks are reentrant. If a thread already holds a monitor, it can re-enter other synchronized methods that use the same monitor without deadlocking. This is essential for inheritance and callback patterns.

public class Parent { public synchronized void doSomething() { System.out.println("Parent"); } } public class Child extends Parent { @Override public synchronized void doSomething() { System.out.println("Child"); super.doSomething(); // Re-enters the same monitor on 'this' } }

The call to super.doSomething() works because the child thread already owns the lock on the Child instance. Without reentrancy, this would deadlock immediately. Reentrancy also allows a synchronized method to call another synchronized method on the same object without issue.

Performance Cost of Method-Level Synchronization

Method-level synchronization is simple but can be expensive under contention. Every call to a synchronized method requires acquiring and releasing the monitor, even when there is no contention. Modern JVMs use biased locking and lightweight locking to reduce this overhead, but under high contention, threads may block and be rescheduled, which adds latency.

More importantly, synchronizing an entire method often holds the lock longer than necessary. If a method performs a long computation or I/O while holding the lock, other threads that need the same lock are blocked even if they only need a small critical section. This can reduce throughput significantly.

Consider a method that reads a configuration value and then performs a slow network call:

public synchronized String getConfig(String key) { String value = configMap.get(key); // short critical section String enriched = fetchFromRemote(value); // long operation, lock held return enriched; }

Here the lock is held during the network call, blocking all other readers. A better design is to synchronize only the map access and perform the remote call outside the lock, or to use a ReadWriteLock if reads dominate.

Common Pitfalls with synchronized Methods

One frequent mistake is synchronizing on a method but not on the object that actually holds the shared state. For example, if you have a collection as a field and you synchronize a method that returns the collection itself, callers can modify the collection without acquiring the lock.

public class ShoppingCart { private List<Item> items = new ArrayList<>(); public synchronized List<Item> getItems() { return items; // Unsafe: caller can mutate without lock } }

Returning a copy or an unmodifiable view avoids this. Another pitfall is assuming that a synchronized method protects all fields of the object. It only protects code that uses the same lock. If some methods are synchronized and others are not, unsynchronized access remains unsafe.

Another subtle issue is lock visibility across classes. If you synchronize on this and another class also synchronizes on the same instance for a different purpose, you may create unintended contention or deadlock. Prefer private lock objects when you need finer control.

When to Prefer synchronized Blocks or Explicit Locks

A synchronized method is appropriate when the entire method body is a short, well-defined critical section. For longer operations or when you need to lock only part of a method, a synchronized block gives you finer granularity and reduces lock hold time.

public void update(String key, String value) { // compute outside lock String normalized = normalize(value); synchronized (this) { map.put(key, normalized); } }

If you need advanced features like timed lock acquisition, interruptible locking, or multiple condition queues, use java.util.concurrent.locks.Lock implementations. A ReentrantLock provides the same reentrancy as intrinsic locks but allows you to try locking without blocking indefinitely.

Choosing between these options depends on the critical section size and the concurrency requirements. For simple atomic updates, synchronized methods are clear and less error-prone. For high contention or complex locking schemes, explicit locks offer more control but require manual unlock handling. Always measure the actual contention before optimizing; premature use of explicit locks can make code harder to maintain without measurable benefit.

java synchronized method: Practical Usage and Code Examples | RYUSLOG DEV