Synchronization in Java Multithreading
Introduction to Synchronization in Java
In Java, synchronization is a mechanism that ensures controlled access to shared resources in a multithreaded environment. When multiple threads access shared resources (e.g., variables, objects, or files) simultaneously, it can lead to data inconsistency or race conditions. Synchronization prevents such issues by allowing only one thread to access a critical section of code at a time.
Java provides built-in synchronization mechanisms to ensure thread safety and maintain data integrity.
Why Synchronization is Needed
In a multithreaded program, threads often share resources like variables or objects. Without synchronization, multiple threads may modify the same resource simultaneously, leading to unpredictable results.
Example of a Race Condition:
class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class RaceConditionExample {
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount());
}
}
Output (varies):
Final count: 1897
In this example, the final count is incorrect because both threads modify the
countvariable simultaneously, causing a race condition.
Types of Synchronization in Java
Java provides two types of synchronization:
- Process Synchronization: Ensures that processes (not threads) execute in a synchronized manner.
- Thread Synchronization: Ensures that threads access shared resources in a synchronized manner. This is the focus of this article.
Thread Synchronization in Java
Thread synchronization can be achieved in the following ways:
- Synchronized Methods
- Synchronized Blocks
- Static Synchronization
- Using Locks (from
java.util.concurrent.lockspackage)
Synchronized Methods
A synchronized method ensures that only one thread can execute it at a time. The lock is associated with the object on which the method is called.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class SynchronizedMethodExample {
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount());
}
}
Output:
Final count: 2000
Synchronized Blocks
A synchronized block allows you to synchronize only a specific part of the code, rather than the entire method. This improves performance by reducing the scope of synchronization.
Syntax:
synchronized(object) {
// Critical section
}
Example:
class Counter {
private int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
public int getCount() {
return count;
}
}
In this example, only the critical section (
count++) is synchronized, improving performance.
Static Synchronization
Static synchronization is used to synchronize static methods. The lock is associated with the class, not the object.
Example
class Counter {
private static int count = 0;
public static synchronized void increment() {
count++;
}
public static int getCount() {
return count;
}
}
public class StaticSynchronizationExample {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
Counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
Counter.increment();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + Counter.getCount());
}
}
Using Locks
The java.util.concurrent.locks package provides the Lock interface, which offers more flexibility than synchronized methods or blocks.
Example:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
public class LockExample {
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount());
}
}
Advantages of Synchronization
- Thread Safety: Prevents data inconsistency and race conditions.
- Data Integrity: Ensures that shared resources are accessed in a controlled manner.
- Simplified Debugging: Reduces unpredictable behavior in multithreaded programs.
Disadvantages of Synchronization
- Performance Overhead: Synchronization can slow down the program due to thread contention.
- Deadlocks: Improper synchronization can lead to deadlocks, where threads wait indefinitely for resources.
- Complexity: Managing synchronized code can make the program harder to understand and maintain.
