Thread signaling and coordination in Java involve mechanisms that allow threads to notify each other about changes in their state, thus enabling smooth and efficient inter-thread communication.
Here’s a available techniques for thread signaling and coordination:
- wait(), notify(), and notifyAll() Methods
- ReentrantLock and Condition Variables
- CountDownLatch
- CyclicBarrier
- Semaphore
1.wait(), notify(), and notifyAll() Methods
These methods, part of the Object class, are fundamental for basic thread coordination:
wait(): Makes the current thread release the lock and enter the waiting state until another thread invokes notify() or notifyAll() on the same object.
notify(): Wakes up a single thread that is waiting on the object’s monitor.
notifyAll(): Wakes up all threads that are waiting on the object’s monitor.
These methods are always used within synchronized blocks or methods to ensure proper access control.
Program
//ProducerConsumerDemo.java class SharedResource { private int data; private boolean isDataAvailable = false; // Track if data is available or not // Synchronized method for producer to produce data public synchronized void produce(int value) { while (isDataAvailable) { try { wait(); // Wait if data is already available } catch (InterruptedException e) { Thread.currentThread().interrupt(); // Handle thread interruption } } data = value; // Assign new data value isDataAvailable = true; // Mark that new data is available System.out.println("Produced: " + value); // Log the produced value notifyAll(); // Notify waiting consumer thread } // Synchronized method for consumer to consume data public synchronized int consume() { while (!isDataAvailable) { try { wait(); // Wait if no data is available yet } catch (InterruptedException e) { Thread.currentThread().interrupt(); // Handle thread interruption } } isDataAvailable = false; // Mark that the data has been consumed System.out.println("Consumed: " + data); // Log the consumed value notifyAll(); // Notify the producer that the data has been consumed return data; // Return the consumed data } } public class ProducerConsumerDemo { public static void main(String[] args) { SharedResource resource = new SharedResource(); // Shared resource for both threads // Producer thread Runnable producerTask = () -> { for (int i = 1; i < 10; i++) { resource.produce(i); // Produce data } }; // Consumer thread Runnable consumerTask = () -> { for (int i = 1; i < 10; i++) { resource.consume(); // Consume data } }; // Create and start both producer and consumer threads Thread producerThread = new Thread(producerTask); Thread consumerThread = new Thread(consumerTask); // Start consumer first to ensure it waits for the producer consumerThread.start(); producerThread.start(); } } /* C:\>javac ProducerConsumerDemo.java C:\>java ProducerConsumerDemo Produced: 1 Consumed: 1 Produced: 2 Consumed: 2 Produced: 3 Consumed: 3 Produced: 4 Consumed: 4 Produced: 5 Consumed: 5 Produced: 6 Consumed: 6 Produced: 7 Consumed: 7 Produced: 8 Consumed: 8 Produced: 9 Consumed: 9 */
2.ReentrantLock and Condition Variables
ReentrantLock is a part of the java.util.concurrent.locks package and offers more flexibility compared to synchronized blocks. With ReentrantLock, you can create multiple Condition objects for finer control over thread coordination.
//ProducerConsumerUsingLocks.java import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class SharedResource { private int data; private boolean isDataAvailable = false; private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); // Producer method public void produce(int value) { lock.lock(); // Acquire lock try { // Wait while data is available (i.e., the consumer hasn't consumed yet) while (isDataAvailable) { condition.await(); // Release the lock and wait for the consumer to signal } // Produce data data = value; isDataAvailable = true; System.out.println("Produced: " + value); // Notify all waiting threads (consumer) that new data is available condition.signalAll(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // Restore the interrupt status } finally { lock.unlock(); // Release the lock } } // Consumer method public int consume() { lock.lock(); // Acquire lock try { // Wait while no data is available (i.e., the producer hasn't produced yet) while (!isDataAvailable) { condition.await(); // Release the lock and wait for the producer to signal } // Consume data isDataAvailable = false; System.out.println("Consumed: " + data); // Notify all waiting threads (producer) that data has been consumed condition.signalAll(); return data; } catch (InterruptedException e) { Thread.currentThread().interrupt(); // Restore the interrupt status return -1; // Handle appropriately } finally { lock.unlock(); // Release the lock } } } public class ProducerConsumerUsingLocks { public static void main(String[] args) { SharedResource resource = new SharedResource(); // Shared resource between producer and consumer // Producer task Runnable producerTask = () -> { for (int i = 1; i < 10; i++) { resource.produce(i); // Produce data } }; // Consumer task Runnable consumerTask = () -> { for (int i = 1; i < 10; i++) { resource.consume(); // Consume data } }; // Create and start producer and consumer threads Thread producerThread = new Thread(producerTask); Thread consumerThread = new Thread(consumerTask); producerThread.start(); consumerThread.start(); try { producerThread.join(); // Wait for producer to finish consumerThread.join(); // Wait for consumer to finish } catch (InterruptedException e) { Thread.currentThread().interrupt(); // Handle thread interruption } } } /* C:\>javac ProducerConsumerUsingLocks.java C:\>java ProducerConsumerUsingLocks Produced: 1 Consumed: 1 Produced: 2 Consumed: 2 Produced: 3 Consumed: 3 Produced: 4 Consumed: 4 Produced: 5 Consumed: 5 Produced: 6 Consumed: 6 Produced: 7 Consumed: 7 Produced: 8 Consumed: 8 Produced: 9 Consumed: 9 */
3.CountDownLatch
CountDownLatch is a synchronization aid that allows one or more threads to wait until a set of operations being performed by other threads completes.
//CountDownLatchDemo.java import java.util.concurrent.CountDownLatch; class Worker extends Thread { private final CountDownLatch latch; public Worker(CountDownLatch latch) { this.latch = latch; } public void run() { System.out.println("Worker is doing work"); try { Thread.sleep(1000); // Simulate work } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Worker finished work"); latch.countDown(); } } public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { int numberOfWorkers = 3; CountDownLatch latch = new CountDownLatch(numberOfWorkers); for (int i = 0; i < numberOfWorkers; i++) { new Worker(latch).start(); } latch.await(); // Main thread waits until all workers finish System.out.println("All workers have finished"); } } /* C:\>javac CountDownLatchDemo.java C:\>java CountDownLatchDemo Worker is doing work Worker is doing work Worker is doing work Worker finished work Worker finished work Worker finished work All workers have finished */
4.CyclicBarrier
CyclicBarrier is a synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.
//CyclicBarrierDemo.java import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; class Worker extends Thread { private final CyclicBarrier barrier; public Worker(CyclicBarrier barrier) { this.barrier = barrier; } @Override public void run() { try { System.out.println(Thread.currentThread().getName() + " is doing work"); Thread.sleep(1000); // Simulate work System.out.println(Thread.currentThread().getName() + " finished work"); barrier.await(); // Wait for all threads to reach this point } catch (InterruptedException | BrokenBarrierException e) { Thread.currentThread().interrupt(); } } } public class CyclicBarrierDemo { public static void main(String[] args) { int numberOfWorkers = 3; CyclicBarrier barrier = new CyclicBarrier(numberOfWorkers, () -> { System.out.println("All workers have reached the barrier"); }); for (int i = 0; i < numberOfWorkers; i++) { new Worker(barrier).start(); } } } /* C:\>javac CyclicBarrierDemo.java C:\>java CyclicBarrierDemo Thread-1 is doing work Thread-2 is doing work Thread-0 is doing work Thread-1 finished work Thread-2 finished work Thread-0 finished work All workers have reached the barrier */
5.Semaphore
A Semaphore controls access to a shared resource through a set number of permits.
//SemaphoreDemo.java import java.util.concurrent.Semaphore; class ResourceUser extends Thread { private final Semaphore semaphore; public ResourceUser(Semaphore semaphore) { this.semaphore = semaphore; } @Override public void run() { try { System.out.println(Thread.currentThread().getName() + " trying to acquire the lock..."); semaphore.acquire(); // Acquire the lock (only one thread can hold it) System.out.println(Thread.currentThread().getName() + " acquired the lock and is using the resource."); // Simulate resource usage Thread.sleep(1000); // Simulate some work } catch (InterruptedException e) { // Handle thread interruption (good practice to restore interrupted status) Thread.currentThread().interrupt(); System.out.println(Thread.currentThread().getName() + " was interrupted!"); } finally { System.out.println(Thread.currentThread().getName() + " released the lock."); semaphore.release(); // Release the lock } } } public class SemaphoreDemo { public static void main(String[] args) { // Semaphore with 1 permit, meaning only one thread can acquire the lock at a time Semaphore semaphore = new Semaphore(1); // One permit for exclusive access for (int i = 0; i < 5; i++) { new ResourceUser(semaphore).start(); } } } /* C:\>javac SemaphoreDemo.java C:\>java SemaphoreDemo Thread-1 trying to acquire the lock... Thread-1 acquired the lock and is using the resource. Thread-3 trying to acquire the lock... Thread-2 trying to acquire the lock... Thread-4 trying to acquire the lock... Thread-0 trying to acquire the lock... Thread-1 released the lock. Thread-3 acquired the lock and is using the resource. Thread-3 released the lock. Thread-2 acquired the lock and is using the resource. Thread-2 released the lock. Thread-4 acquired the lock and is using the resource. Thread-4 released the lock. Thread-0 acquired the lock and is using the resource. Thread-0 released the lock. */
Java provides multiple ways to coordinate and signal between threads, each suited to different scenarios:
- wait(), notify(), notifyAll(): Basic and low-level methods for synchronization within synchronized blocks or methods.
- ReentrantLock and Condition: More flexible and powerful than synchronized blocks.
- CountDownLatch: Used for waiting until a set of operations are completed.
- CyclicBarrier: Used for a group of threads to wait for each other.
- Semaphore: Used to control access to a resource with a fixed number of permits.
Choosing the appropriate mechanism depends on the specific requirements of your application.