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 injava.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.