How Does the Synchronized Block Work in Java?

How Does the Synchronized Block Work in Java?
Want to make your Java code thread-safe? Discover how the synchronized block works in Java with examples, deep explanations, and performance insights. This guide demystifies synchronization and shows you the right way to manage concurrent access to shared resources.

Introduction to the Synchronized Block in Java

In multithreaded programming, ensuring thread safety is crucial when multiple threads access shared resources. Java provides built-in support for thread synchronization using the synchronized keyword. The synchronized block in Java is one of the most powerful tools to control access to critical sections of code.

Why Do We Need Synchronization?

Java allows multiple threads to run concurrently. If these threads access shared data without proper synchronization, it can lead to inconsistent or unexpected results, such as:

  • Race conditions
  • Data corruption
  • Application crashes

The synchronized block helps prevent these issues by allowing only one thread at a time to execute a critical section of code.

What Is a Synchronized Block?

A synchronized block is used to lock a particular object so that only one thread can execute the block at a time for that object. It provides finer-grained control compared to synchronized methods.

public void method() {
    synchronized (lockObject) {
        // critical section
    }
}

Key Points:

  • The lock is on the lockObject.
  • Only one thread can acquire the lock on that object at a time.
  • Other threads must wait until the lock is released.

Example: Basic Synchronized Block

Let’s look at a simple example that uses a synchronized block to update a counter.

public class Counter {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        synchronized (lock) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

Multiple threads can call increment(), but only one thread at a time will be allowed to execute the code inside the synchronized block.

Using This vs. Custom Lock Object

You can use the current instance (this) or a separate lock object:

// Using this
synchronized (this) {
    // critical section
}

// Using custom lock
private final Object lock = new Object();
synchronized (lock) {
    // critical section
}

Why Prefer a Custom Lock Object?

  • Encapsulation – you can hide the lock object.
  • More flexible – lets you synchronize only a specific part of the class.

Synchronized Block vs. Synchronized Method

Feature Synchronized Block Synchronized Method
Scope Only part of the method Entire method
Lock Object Custom object or this Implicitly this or class object (for static)
Performance Better (less code is locked) Can lock too much code

Thread Safety Demonstration

Let’s look at an example with multiple threads trying to update a shared resource:

public class BankAccount {
    private int balance = 1000;
    private final Object lock = new Object();

    public void withdraw(int amount) {
        synchronized (lock) {
            if (balance >= amount) {
                balance -= amount;
            }
        }
    }

    public int getBalance() {
        return balance;
    }
}

This code ensures that no two threads can withdraw money at the same time, which prevents overdraft.

Class Level Locking

To synchronize across all instances of a class, use the class literal:

public void log() {
    synchronized (MyClass.class) {
        // synchronized across all instances
    }
}

Nested Synchronized Blocks

You can nest synchronized blocks, but it increases the risk of deadlock:

synchronized (lock1) {
    synchronized (lock2) {
        // risky if threads acquire locks in different order
    }
}

Deadlock Example

Thread 1:             Thread 2:
synchronized (A) {    synchronized (B) {
    synchronized (B)      synchronized (A)
}

Both threads will wait for each other forever. To avoid deadlocks:

  • Always acquire locks in the same order.
  • Minimize the use of nested locks.

Reentrancy in Synchronized Blocks

Java’s synchronization is reentrant, meaning the same thread can enter the block multiple times if it already holds the lock:

public class ReentrantExample {
    public synchronized void outer() {
        inner();
    }

    public synchronized void inner() {
        // allowed, same thread holds the lock
    }
}

Performance Considerations

Synchronized blocks can impact performance if used excessively. Tips to optimize:

  • Keep critical sections short.
  • Avoid I/O operations inside synchronized blocks.
  • Use concurrent collections if available (e.g., ConcurrentHashMap).

When to Use Synchronized Blocks

  • When you need mutual exclusion for only a portion of a method.
  • When you need more control over the lock object.
  • To improve performance over synchronized methods.

Real-world Use Case

Suppose you’re building a multi-threaded logging system:

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

    public void log(String message) {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + ": " + message);
        }
    }
}

This ensures logs don’t get interleaved across threads.

Best Practices

  • Prefer synchronized blocks over synchronized methods.
  • Always use a private final lock object.
  • Keep synchronized sections as short as possible.
  • Avoid using String literals as lock objects (they can be shared).

Alternatives to Synchronized

For more advanced control, Java provides:

  • ReentrantLock
  • ReadWriteLock
  • AtomicInteger and other classes in java.util.concurrent.atomic

Conclusion

The synchronized block in Java is a fundamental tool for writing thread-safe code. While it’s powerful, it’s also essential to use it wisely to prevent performance issues and deadlocks. With clear understanding and careful implementation, synchronized blocks help you build robust, concurrent Java applications.

Please follow and like us:

Leave a Comment