4.1.3 Thread Synchronization Explained
Thread synchronization in Java is a mechanism that ensures multiple threads access shared resources in a controlled manner, preventing data inconsistency and race conditions. Understanding thread synchronization is crucial for writing robust and thread-safe Java applications.
Key Concepts
1. Race Condition
A race condition occurs when two or more threads access a shared resource simultaneously, leading to unpredictable results. Synchronization is used to prevent race conditions by ensuring that only one thread can access the shared resource at a time.
Example
class Counter { private int count = 0; public void increment() { count++; } public int getCount() { return count; } } public class Main { public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Runnable task = () -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.getCount()); // Output may vary, not always 2000 } }
2. Synchronized Methods
Synchronized methods are methods that can only be executed by one thread at a time. When a thread invokes a synchronized method, it acquires the intrinsic lock for that method's object, preventing other threads from executing any synchronized methods on the same object.
Example
class SynchronizedCounter { private int count = 0; public synchronized void increment() { count++; } public synchronized int getCount() { return count; } } public class Main { public static void main(String[] args) throws InterruptedException { SynchronizedCounter counter = new SynchronizedCounter(); Runnable task = () -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.getCount()); // Output: 2000 } }
3. Synchronized Blocks
Synchronized blocks allow you to synchronize only a part of a method or a block of code. This provides more granular control over synchronization and can improve performance by reducing the scope of synchronization.
Example
class SynchronizedBlockCounter { private int count = 0; public void increment() { synchronized (this) { count++; } } public int getCount() { synchronized (this) { return count; } } } public class Main { public static void main(String[] args) throws InterruptedException { SynchronizedBlockCounter counter = new SynchronizedBlockCounter(); Runnable task = () -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.getCount()); // Output: 2000 } }
4. Reentrant Locks
Reentrant Locks provide a more flexible alternative to synchronized methods and blocks. They allow you to explicitly lock and unlock resources, providing more control over synchronization. Reentrant Locks also support features like fairness and condition variables.
Example
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class ReentrantLockCounter { private int count = 0; private Lock lock = new ReentrantLock(); public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } public int getCount() { lock.lock(); try { return count; } finally { lock.unlock(); } } } public class Main { public static void main(String[] args) throws InterruptedException { ReentrantLockCounter counter = new ReentrantLockCounter(); Runnable task = () -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.getCount()); // Output: 2000 } }
Examples and Analogies
Think of thread synchronization as a traffic light at an intersection. Just as a traffic light ensures that cars (threads) pass through the intersection one at a time, synchronization ensures that threads access shared resources in a controlled manner. Without synchronization, multiple threads could collide (race condition), leading to chaos and accidents (data inconsistency).
By mastering thread synchronization, you can write Java applications that are both efficient and thread-safe, ensuring that your code behaves predictably even in a multi-threaded environment.