Practical Java MultiThreading: Semaphore and Barrier Operations

Multithreading is a powerful concept in Java that allows a program to perform multiple tasks concurrently, thus improving the overall performance and responsiveness of the application. Two important synchronization tools in Java’s multithreading toolkit are Semaphores and Barriers. Semaphores are used to control access to a limited number of resources. They can be thought of as a set of permits; a thread must acquire a permit to access a resource and release it when done. Barriers, on the other hand, are used to synchronize multiple threads at a certain point in their execution. All threads must reach the barrier before any of them can proceed further. In this blog, we will explore the fundamental concepts of Semaphores and Barriers in Java, their usage methods, common practices, and best practices.

Table of Contents

  1. Fundamental Concepts
    • Semaphores
    • Barriers
  2. Usage Methods
    • Semaphores
    • Barriers
  3. Common Practices
    • Semaphores
    • Barriers
  4. Best Practices
    • Semaphores
    • Barriers
  5. Conclusion
  6. References

Fundamental Concepts

Semaphores

A semaphore is a synchronization primitive that controls access to a shared resource. It maintains a set of permits. When a thread wants to access the resource, it must acquire a permit from the semaphore. If a permit is available, the thread can proceed and the number of available permits is decreased. When the thread is done with the resource, it releases the permit, increasing the number of available permits.

There are two types of semaphores: binary semaphores and counting semaphores. Binary semaphores have only one permit, and they are often used to implement mutual exclusion (similar to synchronized blocks). Counting semaphores can have multiple permits and are used to control access to a resource with a limited number of instances.

Barriers

A barrier is a synchronization aid that allows a set of threads to wait for each other to reach a common point in their execution. When a thread reaches the barrier, it waits until all other threads in the set also reach the barrier. Once all threads have reached the barrier, they can all proceed.

Java provides two types of barriers: CyclicBarrier and CountDownLatch. A CyclicBarrier can be reused after all threads have reached the barrier. A CountDownLatch is a one - time use barrier; once the count reaches zero, the waiting threads are released and the latch cannot be reused.

Usage Methods

Semaphores

In Java, semaphores are implemented using the java.util.concurrent.Semaphore class. Here is a simple example of using a semaphore to control access to a limited number of resources:

import java.util.concurrent.Semaphore;

class Resource {
    private static final int MAX_USERS = 3;
    private Semaphore semaphore = new Semaphore(MAX_USERS);

    public void useResource() {
        try {
            // Acquire a permit
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + " is using the resource.");
            // Simulate some work
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // Release the permit
            semaphore.release();
            System.out.println(Thread.currentThread().getName() + " has released the resource.");
        }
    }
}

public class SemaphoreExample {
    public static void main(String[] args) {
        Resource resource = new Resource();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> resource.useResource()).start();
        }
    }
}

In this example, we create a semaphore with 3 permits. Only 3 threads can access the resource at the same time. When a thread finishes using the resource, it releases the permit, allowing another thread to acquire it.

Barriers

CyclicBarrier

The CyclicBarrier class in Java is used to create a reusable barrier. Here is an example:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

class Task implements Runnable {
    private CyclicBarrier barrier;

    public Task(CyclicBarrier barrier) {
        this.barrier = barrier;
    }

    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName() + " is waiting at the barrier.");
            barrier.await();
            System.out.println(Thread.currentThread().getName() + " has passed the barrier.");
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}

public class CyclicBarrierExample {
    public static void main(String[] args) {
        int numberOfThreads = 3;
        CyclicBarrier barrier = new CyclicBarrier(numberOfThreads);

        for (int i = 0; i < numberOfThreads; i++) {
            new Thread(new Task(barrier)).start();
        }
    }
}

In this example, we create a CyclicBarrier with 3 threads. Each thread waits at the barrier until all 3 threads have reached it. Then, all threads can proceed.

CountDownLatch

The CountDownLatch class is used to create a one - time use barrier. Here is an example:

import java.util.concurrent.CountDownLatch;

class Worker implements Runnable {
    private CountDownLatch latch;

    public Worker(CountDownLatch latch) {
        this.latch = latch;
    }

    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName() + " is working.");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " has finished working.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // Decrement the latch count
            latch.countDown();
        }
    }
}

public class CountDownLatchExample {
    public static void main(String[] args) {
        int numberOfWorkers = 3;
        CountDownLatch latch = new CountDownLatch(numberOfWorkers);

        for (int i = 0; i < numberOfWorkers; i++) {
            new Thread(new Worker(latch)).start();
        }

        try {
            // Wait for all workers to finish
            latch.await();
            System.out.println("All workers have finished.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

In this example, we create a CountDownLatch with a count of 3. Each worker thread decrements the count when it finishes working. The main thread waits until the count reaches zero.

Common Practices

Semaphores

  • Resource Pooling: Semaphores are commonly used to implement resource pooling. For example, a connection pool for a database can use a semaphore to limit the number of concurrent connections.
  • Rate Limiting: Semaphores can be used to limit the rate of requests to a service. For example, a web server can use a semaphore to limit the number of concurrent requests it can handle.

Barriers

  • Parallel Computation: Barriers are useful in parallel computation scenarios. For example, in a parallel sorting algorithm, different threads can perform sorting on different parts of the data. A barrier can be used to ensure that all threads have finished sorting their parts before merging the results.
  • Initialization: Barriers can be used to ensure that all necessary components are initialized before the main application starts.

Best Practices

Semaphores

  • Always Release Permits: Always release the semaphore permits in a finally block to ensure that they are released even if an exception occurs.
  • Use Appropriate Permit Count: Set the number of permits based on the actual number of available resources. Do not over - or under - allocate permits.

Barriers

  • Handle Exceptions Properly: When using barriers, handle InterruptedException and BrokenBarrierException properly. These exceptions can occur if a thread is interrupted or if the barrier is broken.
  • Choose the Right Barrier Type: Choose between CyclicBarrier and CountDownLatch based on whether you need a reusable barrier or a one - time use barrier.

Conclusion

Semaphores and barriers are powerful synchronization tools in Java’s multithreading toolkit. Semaphores are used to control access to limited resources, while barriers are used to synchronize multiple threads at a common point in their execution. By understanding their fundamental concepts, usage methods, common practices, and best practices, developers can write more efficient and reliable multithreaded Java applications.

References