Back to Blog
Java

Java Synchronized Block: Scope, Locking, and Tradeoffs

Learn how java synchronized block controls access to critical sections, how to choose lock objects, and when to use it over synchronized methods.

Java concurrencysynchronizedthread safetylockingcritical section
Illustation of a Java synchronized block guarding a critical section with a lock icon.

A java synchronized block is the most direct way to protect a critical section from concurrent access. It lets you define exactly which statements need mutual exclusion and which object's lock guards them. Unlike a synchronized method, which locks the entire method, a block gives you finer control over the locked region and the lock instance.

What a synchronized Block Protects

The synchronized keyword in Java is built around the concept of an intrinsic lock, also called a monitor. Every Java object has one. When a thread enters a synchronized block, it acquires the lock for the object you specify. While the lock is held, no other thread can enter any synchronized block that uses the same lock. When the thread exits the block, the lock is released automatically, even if an exception is thrown.

The block protects only the statements inside it. Code before or after the block runs without holding the lock. That distinction matters because it lets you minimize the time a lock is held, which reduces contention and improves throughput.

Syntax and Minimal Example

The syntax of a synchronized block is straightforward:

synchronized (lockObject) { // critical section }

The lockObject is any reference to an object. The block will acquire that object's monitor before executing the statements inside. Here is a minimal counter that uses a synchronized block to make increments atomic:

public class Counter { private int count = 0; private final Object lock = new Object(); public void increment() { synchronized (lock) { count++; } } public int getCount() { synchronized (lock) { return count; } } }

In this example, the lock object is a private, final instance field. Using a dedicated lock object prevents external code from interfering with your locking strategy. If you synchronized on this instead, any code that holds a reference to the Counter instance could also synchronize on the same lock, potentially causing unexpected contention or deadlock.

Choosing the Lock Object

The lock object you choose determines the scope of mutual exclusion. If two threads synchronize on different objects, they do not block each other. This is useful when you want to allow concurrent access to different data structures that are protected by separate locks.

A common pattern is to use a dedicated lock object for each resource you want to protect. For example:

public class BankAccount { private final Object balanceLock = new Object(); private final Object transactionLock = new Object(); private double balance; private List<String> transactions = new ArrayList<>(); public void deposit(double amount) { synchronized (balanceLock) { balance += amount; } synchronized (transactionLock) { transactions.add("Deposit: " + amount); } } }

Here, the balance and the transaction list are protected by different locks. A thread updating the balance does not block a thread reading the transaction list, as long as they never need to access both together. This reduces contention but requires careful design to avoid inconsistent reads when both fields must be updated atomically.

When the lock object is this, the block is equivalent to a synchronized method in terms of lock scope. However, using a private lock object is generally safer because it prevents external code from acquiring the same lock and causing unintended blocking.

Reentrancy and Lock Behavior

Java intrinsic locks are reentrant. If a thread already holds a lock, it can acquire the same lock again without blocking. This is essential for code that calls other synchronized methods or blocks on the same object.

public class ReentrantExample { private final Object lock = new Object(); public void outer() { synchronized (lock) { inner(); } } public void inner() { synchronized (lock) { // This is allowed because the thread already holds the lock. } } }

The JVM tracks the number of times a lock has been acquired by the same thread. Each synchronized block entry increments a counter, and each exit decrements it. The lock is released only when the counter returns to zero. This behavior is what makes recursive or nested synchronized blocks safe.

Reentrancy also means that a thread cannot deadlock with itself. If intrinsic locks were not reentrant, a method that calls another synchronized method on the same object would deadlock immediately.

Performance and Contention

Synchronized blocks have a runtime cost. Acquiring and releasing a lock requires JVM-level operations, and contention adds overhead. The key performance concern is how long the lock is held. A synchronized block that contains only a few operations is cheaper than one that wraps a long-running computation.

When multiple threads contend for the same lock, the JVM may use biased locking, lightweight locking, or heavy monitor locking depending on the situation. Modern JVMs optimize uncontended locks heavily, so the overhead is often small. However, under high contention, threads may block and be descheduled, which can cause significant latency spikes.

To minimize contention, keep synchronized blocks as short as possible. Move I/O, network calls, or expensive computations outside the block. If you need to protect a larger operation that cannot be split easily, consider using java.util.concurrent utilities such as ReentrantLock which offer more control, like timed lock acquisition or fairness policies. But for simple mutual exclusion, a synchronized block is often sufficient and more readable.

Common Mistakes and Pitfalls

A frequent mistake is synchronizing on a mutable field that can change. If the lock object reference is reassigned, threads may end up locking on different objects, defeating the purpose. Always use a final lock object or an object that is never reassigned.

Another mistake is using a string literal as a lock. String literals are interned, so two unrelated parts of the code that use the same literal will share the same lock. This can cause unexpected blocking across classes. For example:

synchronized ("lock") { // Dangerous: all threads using "lock" share the same monitor }

This is rarely what you want. Use a private final object instead.

A third issue is synchronizing on a Boolean or Integer wrapper. Autoboxing can create new objects, so the lock identity may change. For instance, synchronized (count) where count is an Integer can lock on different objects if the value changes. Always use a dedicated lock object.

Finally, be careful about lock ordering when multiple locks are acquired. If thread A holds lock X and waits for lock Y, while thread B holds lock Y and waits for lock X, you have a deadlock. Synchronized blocks do not provide any ordering guarantee, so you must design your code to acquire locks in a consistent global order.

Synchronized Block vs Synchronized Method

A synchronized method is equivalent to a synchronized block that uses this as the lock for an instance method, or the Class object for a static method. The difference is granularity. A synchronized method locks the entire method body, which may be more than necessary.

public synchronized void update() { // entire method is locked }

This is the same as:

public void update() { synchronized (this) { // entire method is locked } }

If only a few lines inside the method need protection, a synchronized block allows you to reduce the lock scope. It also lets you choose a different lock object, which can improve concurrency when different methods protect different resources.

However, a synchronized method is simpler to write and less error-prone for small methods. For a method that is short and entirely critical, the block adds no benefit. The choice depends on whether you need to control the lock object or the extent of the locked region.

In production code, you often see synchronized blocks used inside larger methods where only a small section modifies shared state. This pattern keeps the critical section short and reduces the chance of contention with unrelated operations.

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