10.2 Synchronization and Thread Safety
Overview
When working with multithreading, thread safety is crucial to ensure that data consistency is maintained even when multiple threads access shared resources. Java provides several mechanisms to handle thread synchronization, which helps prevent data corruption and ensures predictable program behavior.
1. The Synchronized Keyword
The synchronized keyword in Java is used to control access to a method or block of code by only one thread at a time. It ensures that only one thread can execute the synchronized block or method, maintaining thread safety.
Example: Synchronized Method
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
public static void main(String[] args) {
Counter counter = new Counter();
// Creating multiple threads
Thread t1 = new Thread(counter::increment);
Thread t2 = new Thread(counter::increment);
t1.start();
t2.start();
// Wait for threads to finish
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final Count: " + counter.getCount());
}
}
In this example, the increment method is synchronized, ensuring that only one thread at a time can modify count, maintaining consistency.
2. Synchronized Block
A synchronized block is a more flexible way to synchronize specific sections of code rather than an entire method. This allows finer control, improving efficiency.
Example: Synchronized Block
public class BankAccount {
private int balance = 1000;
public void deposit(int amount) {
synchronized(this) {
balance += amount;
}
}
public int getBalance() {
return balance;
}
}
In this example, only the block within deposit is synchronized, allowing for efficient access to non-critical code outside the synchronized block.
3. The Volatile Keyword
The volatile keyword is used to mark a variable as modified by multiple threads. It ensures that a thread reading a volatile variable sees the latest written value, helping to prevent visibility issues.
Example: Using Volatile
public class FlagExample {
private volatile boolean flag = true;
public void stop() {
flag = false;
}
public void run() {
while (flag) {
System.out.println("Running...");
}
}
public static void main(String[] args) {
FlagExample example = new FlagExample();
new Thread(example::run).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
example.stop();
System.out.println("Stopped");
}
}
In this example, flag is marked as volatile to ensure the value is updated immediately across all threads.
4. Using Locks
The Lock interface provides more control over synchronization, allowing more flexible thread-safe operations compared to synchronized.
Example: ReentrantLock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
public static void main(String[] args) {
LockExample example = new LockExample();
// Increment using multiple threads
Thread t1 = new Thread(example::increment);
Thread t2 = new Thread(example::increment);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final Count: " + example.getCount());
}
}
In this example, we use ReentrantLock to manage the increment operation, offering fine-grained control over locking and unlocking.
5. ReadWriteLock
The ReadWriteLock interface allows multiple readers but restricts access to a single writer, improving performance in read-heavy applications.
Example: Using ReadWriteLock
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteExample {
private int data = 0;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public void write(int value) {
lock.writeLock().lock();
try {
data = value;
} finally {
lock.writeLock().unlock();
}
}
public int read() {
lock.readLock().lock();
try {
return data;
} finally {
lock.readLock().unlock();
}
}
}
In this example, ReadWriteLock allows multiple threads to read data but only one thread can modify the data at a time, improving performance for read-intensive applications.
Summary
- Use
synchronizedto control access to critical sections in code and prevent data inconsistency. volatileensures visibility of shared variables across threads.- The
LockandReadWriteLockinterfaces provide more flexibility and efficiency for managing thread-safe operations.
Mastering synchronization techniques ensures data integrity in multithreaded applications, enhancing both performance and reliability.