In Java, critical sections and mutual exclusion are essential concepts for handling concurrent programming and ensuring thread safety. A critical section is a part of the code that accesses a shared resource and must not be executed by more than one thread at a time. Mutual exclusion ensures that only one thread accesses the critical section at a time, preventing data inconsistencies and race conditions.
Java provides several mechanisms to achieve mutual exclusion:
- Synchronized Methods
- Synchronized Blocks
- Reentrant Locks
Synchronized Methods
The simplest way to implement mutual exclusion in Java is by using the synchronized keyword.
// Syntax for instance synchronized method
synchronized returnType methodName(parameters) {
// method body
}
Code language: JavaScript (javascript)
 Program Implementation
  In this program, the increment method is synchronized, ensuring that only one thread can execute it at a time.
//SynMethCounter.java public class SynMethCounter { private int count = 0; // Synchronized method to ensure mutual exclusion public synchronized void increment() { count++; } public int getCount() { return count; } public static void main(String[] args) { SynMethCounter counter = new SynMethCounter(); Runnable task = () -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }; Thread thread1 = new Thread(task); Thread thread2 = new Thread(task); thread1.start(); thread2.start(); try { thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Final count: " + counter.getCount()); } } /* C:\>javac SynMethCounter.java C:\>java SynMethCounter Final count: 2000 */
 Synchronized Blocks
synchronized (lockObject) {
// critical section code
}
Code language: JavaScript (javascript)
Program Implementation
In this program, we use a synchronized block with a specific lock object to control access to the critical section.
//SynBlockCounter.java class SynBlockCounter { private int count = 0; private final Object lock = new Object(); // Synchronized block to ensure mutual exclusion public void increment() { synchronized (lock) { count++; } } public int getCount() { return count; } public static void main(String[] args) { Counter counter = new Counter(); Runnable task = () -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }; Thread thread1 = new Thread(task); Thread thread2 = new Thread(task); thread1.start(); thread2.start(); try { thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Final count: " + counter.getCount()); } } /* C:\>javac SynBlockCounter.java C:\>java SynBlockCounter Final count: 2000 */
Reentrant Locks
The ReentrantLock is used to provide mutual exclusion. The lock method locks the critical section, and the unlock method releases the lock, ensuring that only one thread can execute the critical section at a time.
import java.util.concurrent.locks.ReentrantLock;
// Create a ReentrantLock
ReentrantLock lock = new ReentrantLock();
try {
lock.lock(); // Acquire the lock
// critical section code
} finally {
lock.unlock(); // Always release the lock in finally block
}
Code language: JavaScript (javascript)
Program Implementation
//ReentrantCounter.java import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ReentrantCounter { 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) { Counter counter = new Counter(); Runnable task = () -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }; Thread thread1 = new Thread(task); Thread thread2 = new Thread(task); thread1.start(); thread2.start(); try { thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Final count: " + counter.getCount()); } } /* C:\>javac ReentrantCounter.java C:\>java ReentrantCounter Final count: 2000 */
Synchronized Methods and Blocks are Simple to use, but with less flexibility. Re-entrant Locks are more control over locking and unlocking, allowing for more complex synchronization scenarios.Both methods ensure that the critical section is accessed by only one thread at a time, maintaining data consistency and preventing race conditions.