Java > Concurrency and Multithreading > Synchronization and Locks > Synchronized Methods and Blocks
Synchronized Methods: Controlling Access to Shared Resources
This code snippet demonstrates how to use synchronized methods in Java to control access to shared resources, preventing race conditions and ensuring data consistency in a multithreaded environment.
Code Snippet: Synchronized Method Example
This code defines a `Counter` class with a `count` variable. The `increment()` and `getCount()` methods are synchronized. When a thread calls a synchronized method on an object, it acquires the lock associated with that object. No other thread can execute any synchronized method on the same object until the first thread releases the lock. This prevents multiple threads from modifying the `count` variable simultaneously, avoiding race conditions.
class Counter {
private int count = 0;
// Synchronized method
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
class SynchronizedMethodExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Final count: " + counter.getCount()); // Expected: 2000
}
}
Concepts Behind Synchronized Methods
Real-Life Use Case
Consider a banking application where multiple threads might try to update the balance of a shared account simultaneously. Using synchronized methods on methods that modify the account balance (e.g., deposit, withdraw) can prevent overdrafts or incorrect balances due to race conditions.
Best Practices
Interview Tip
Be prepared to explain how `synchronized` works, the concept of intrinsic locks, and the potential for deadlocks when using multiple locks. Also be ready to discuss the performance implications of `synchronized` and alternatives such as `java.util.concurrent` classes like `AtomicInteger` or `ReentrantLock`.
When to Use Synchronized Methods
Use synchronized methods when you need to ensure that only one thread at a time can access and modify the state of an object. It is a simple and effective way to achieve thread safety for simple scenarios.
Memory Footprint
Synchronized methods themselves don't significantly increase the memory footprint. The memory overhead is primarily related to the object's monitor, which is a small amount of metadata associated with each object in the JVM. However, excessive locking can indirectly affect performance and thus resource consumption (CPU, memory due to increased context switching).
Alternatives
Pros
Cons
Synchronized Blocks: Finer-Grained Control
This example uses a synchronized block to protect a critical section of code. Instead of synchronizing the entire method, only the block that modifies the `counter` is synchronized. This allows other parts of the `performTask()` method to execute concurrently, potentially improving performance. The `lock` object is used as the monitor for the synchronized block. Any object can be used as a lock.
class SharedResource {
private int counter = 0;
private final Object lock = new Object();
public void performTask() {
// Some unsynchronized operations
System.out.println("Thread " + Thread.currentThread().getName() + ": Before synchronized block");
synchronized (lock) {
// Critical section: Only one thread can execute this block at a time
System.out.println("Thread " + Thread.currentThread().getName() + ": Inside synchronized block");
counter++;
// Simulate some work
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Thread " + Thread.currentThread().getName() + ": Counter incremented to " + counter);
}
// Some more unsynchronized operations
System.out.println("Thread " + Thread.currentThread().getName() + ": After synchronized block");
}
public int getCounter() {
return counter;
}
}
public class SynchronizedBlockExample {
public static void main(String[] args) throws InterruptedException {
SharedResource resource = new SharedResource();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
resource.performTask();
}
}, "Thread-1");
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
resource.performTask();
}
}, "Thread-2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Final Counter Value: " + resource.getCounter()); // Expected: 10
}
}
FAQ
-
What happens if I synchronize on `this`?
Synchronizing on `this` is equivalent to synchronizing the entire method if the synchronized block encompasses the entire method body. It means the intrinsic lock of the object on which the method is called will be acquired. -
Can I synchronize on a `null` object?
No, attempting to synchronize on a `null` object will throw a `NullPointerException`. -
How does synchronization prevent race conditions?
Synchronization ensures that only one thread can execute the synchronized code at a time. This prevents multiple threads from accessing and modifying shared data concurrently, which eliminates race conditions and ensures data consistency. -
What is a deadlock, and how can it be avoided?
A deadlock occurs when two or more threads are blocked indefinitely, waiting for each other to release locks. To avoid deadlocks, ensure that threads acquire locks in a consistent order and avoid holding multiple locks for extended periods. Use techniques like lock ordering or timeouts to prevent deadlocks.