Introduction to Synchronization
In modern applications, multithreading is an essential feature that enhances performance by allowing multiple threads to run concurrently. However, concurrent access to shared resources can lead to data inconsistency and unexpected behaviors, known as race conditions. To prevent these scenarios, synchronization techniques become crucial.
This blog will provide a comprehensive overview of various synchronization methods available in Java.
1. Intrinsic Locks (Monitor Locks)
Every object in Java has an intrinsic lock, also known as a monitor. When a thread acquires the lock on an object, all other threads that try to access the same object must wait until the lock is released.
Example:
class Counter { private int count = 0; public synchronized void increment() { count++; } public synchronized int getCount() { return count; } }
In the code above, the increment and getCount methods are synchronized. This ensures that only one thread can execute either of these methods at a time, preventing concurrent modifications to the count variable.
2. Synchronized Blocks
Sometimes, you may want only a part of a method to be synchronized. In such cases, synchronized blocks provide a finer level of control, allowing you to limit the scope of synchronization.
Example:
class SharedResource { private int data; public void updateData(int value) { synchronized(this) { data = value; } } }
Here, only the code within the synchronized block is locked, allowing other parts of the updateData method to execute concurrently without blocking.
3. The volatile Keyword
The volatile keyword in Java signifies that a variable's value may be changed by different threads. Marking a variable as volatile ensures that reads and writes to it are directly read from and written to main memory, thus preventing caching effects.
Example:
class VolatileExample { private volatile boolean running = true; public void stop() { running = false; } public void run() { while (running) { // Do something } } }
In this example, the running variable is marked volatile. Changes made by one thread are immediately visible to other threads, making it perfect for flags that control the execution of threads.
4. Reentrant Locks
Java provides the java.util.concurrent.locks package, which includes the ReentrantLock class. This lock is more flexible than intrinsic locks, allowing features like try-lock and timed lock.
Example:
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class LockExample { private Lock lock = new ReentrantLock(); private int count = 0; public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } public int getCount() { return count; } }
The above example demonstrates how to utilize ReentrantLock. Here, we explicitly acquire and release the lock using the lock() and unlock() methods, allowing for better handling of exceptions through the finally block.
5. Semaphores
A semaphore is a signaling mechanism that helps control access to a particular resource by multiple threads. In Java, the Semaphore class is used to maintain a set number of permits for resource access.
Example:
import java.util.concurrent.Semaphore; class SemaphoreExample { private final Semaphore semaphore = new Semaphore(2); // Allow 2 threads to access a resource public void accessResource() { try { semaphore.acquire(); // critical section } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { semaphore.release(); } } }
In this example, the semaphore is initialized with a count of 2, allowing two threads to access the critical section concurrently. Other threads will have to wait until a permit becomes available.
6. CountDownLatch
A CountDownLatch is a synchronization aid that allows one or more threads to wait until a set of operations performed by other threads completes. You initialize it with a count, and each time a thread completes its task, it decrements the count.
Example:
import java.util.concurrent.CountDownLatch; class Task implements Runnable { private CountDownLatch latch; public Task(CountDownLatch latch) { this.latch = latch; } @Override public void run() { try { // Simulate some work Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); } } } public class CountDownLatchExample { public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(3); for (int i = 0; i < 3; i++) { new Thread(new Task(latch)).start(); } latch.await(); // Wait for the count to reach zero System.out.println("All tasks completed!"); } }
In this example, three tasks are started, and the main thread waits for all tasks to complete before printing a message. The CountDownLatch effectively manages synchronization without requiring excessive locking.
7. CyclicBarrier
A CyclicBarrier allows a set of threads to all wait for each other to reach a common barrier point. Once all threads reach the barrier, they are released simultaneously to continue execution.
Example:
import java.util.concurrent.CyclicBarrier; class Worker implements Runnable { private CyclicBarrier barrier; public Worker(CyclicBarrier barrier) { this.barrier = barrier; } @Override public void run() { System.out.println("Worker is doing some work..."); try { Thread.sleep(1000); // Simulate work barrier.await(); // Wait at the barrier } catch (Exception e) { Thread.currentThread().interrupt(); } System.out.println("Worker has finished work and proceeding..."); } } public class CyclicBarrierExample { public static void main(String[] args) throws Exception { final int NUM_WORKERS = 3; CyclicBarrier barrier = new CyclicBarrier(NUM_WORKERS); for (int i = 0; i < NUM_WORKERS; i++) { new Thread(new Worker(barrier)).start(); } } }
In this illustration, each Worker thread performs some work and waits at the barrier for all workers to finish before continuing. This is perfect for scenarios where tasks can proceed only once all threads are ready.
By utilizing these different synchronization techniques in Java, you can effectively manage multithreading in your applications while ensuring data integrity and preventing race conditions.