Skip to main content

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 synchronized to control access to critical sections in code and prevent data inconsistency.
  • volatile ensures visibility of shared variables across threads.
  • The Lock and ReadWriteLock interfaces provide more flexibility and efficiency for managing thread-safe operations.

Mastering synchronization techniques ensures data integrity in multithreaded applications, enhancing both performance and reliability.