โ† Back to Multithreading Course

JAVA MULTITHREADING ยท CLASS 25

Limiting Access with Semaphore

What if multiple threads want to use the same resource, but we want to allow only a limited number of them at a time? Let's understand how Java's Semaphore helps us manage this.

1. Understanding the Problem

Imagine a parking area that has only three parking spaces.

Ten cars arrive, but only three cars can park at the same time. The remaining cars must wait until a parking space becomes available.

The parking area needs a way to control how many cars can enter at once.

In multithreading, we face similar situations.

Imagine ten threads trying to access a resource, but we want only three threads to use it simultaneously.

This is where Semaphore becomes useful.

2. What Is Semaphore?

Semaphore is a Java utility that controls how many threads can access a particular resource at the same time.

It does this using something called permits.

Think of permits as entry passes.

If we have three permits, up to three threads can acquire permits and access the protected resource simultaneously.

When a thread wants to access the resource, it must acquire a permit. When it finishes, it releases that permit so another thread can use it.

Let's understand this with a simple example:

  • We have three permits.
  • Thread 1 acquires a permit.
  • Thread 2 acquires a permit.
  • Thread 3 acquires a permit.
  • Thread 4 tries to acquire a permit but must wait.
  • When one thread releases its permit, Thread 4 can proceed.

This way, we limit the number of threads that can hold permits at the same time.

3. Creating a Semaphore

Semaphore belongs to the java.util.concurrent package.

Let's create a Semaphore with three permits:

import java.util.concurrent.Semaphore;

Semaphore semaphore = new Semaphore(3);

The number 3 means that three permits are initially available.

Now let's understand its two important methods.

acquire()

A thread calls acquire() when it wants to obtain a permit.

semaphore.acquire();

If a permit is available, the thread gets it and continues.

If no permit is available, the thread waits until another thread releases one or the waiting thread is interrupted.

release()

A thread calls release() when it has finished using the resource and wants to return its permit.

semaphore.release();

The released permit becomes available to another waiting thread.

4. Let's Write Our First Program

We will create five worker threads, but we will allow only two threads to access a shared resource at the same time.

We will use Semaphore with two permits.

import java.util.concurrent.Semaphore;

public class SemaphoreExample {

    public static void main(String[] args) {

        Semaphore semaphore = new Semaphore(2);

        for (int i = 1; i <= 5; i++) {

            final int workerNumber = i;

            Thread worker = new Thread(() -> {

                try {

                    System.out.println(
                        "Worker " + workerNumber + " is waiting"
                    );

                    semaphore.acquire();

                    try {

                        System.out.println(
                            "Worker " + workerNumber + " is using the resource"
                        );

                        Thread.sleep(2000);

                    } finally {

                        semaphore.release();

                        System.out.println(
                            "Worker " + workerNumber + " released the resource"
                        );
                    }

                } catch (InterruptedException e) {

                    Thread.currentThread().interrupt();

                    System.out.println(
                        "Worker " + workerNumber + " was interrupted"
                    );
                }
            });

            worker.start();
        }
    }
}

5. Understanding the Program Step by Step

Step 1: Create a Semaphore with two permits

Semaphore semaphore = new Semaphore(2);

We create two permits. This means a maximum of two threads can acquire permits and access the protected resource at the same time.

Step 2: Create five worker threads

for (int i = 1; i <= 5; i++) {
    // Create and start five worker threads
}

We start five threads. All five want to use the same resource.

Step 3: Acquire a permit

semaphore.acquire();

Before using the resource, each worker must acquire a permit.

Since we have only two permits, only two workers can acquire permits at the same time.

The remaining workers wait until a permit becomes available.

Step 4: Use the resource

System.out.println(
    "Worker " + workerNumber + " is using the resource"
);

Thread.sleep(2000);

Once a worker acquires a permit, it can use the resource. We simulate that work using sleep().

Step 5: Release the permit

finally {
    semaphore.release();
}

After finishing its work, the worker releases the permit.

We use a finally block so that the permit is returned even if the worker is interrupted while performing its work.

One important detail: acquire() is placed before the inner try block. If acquire() is interrupted before obtaining a permit, we must not release a permit that the thread never acquired.

6. Understanding the Execution Flow

Let's imagine that all five workers start around the same time.

Available permits: 2

Worker 1 acquires a permit.
Available permits: 1

Worker 2 acquires a permit.
Available permits: 0

Worker 3 tries to acquire a permit and waits.
Worker 4 tries to acquire a permit and waits.
Worker 5 tries to acquire a permit and waits.

Worker 1 finishes and releases a permit.
Available permits: 1

A waiting worker can now acquire that permit.

Worker 2 finishes and releases a permit.
Another waiting worker can proceed.

The exact order in which waiting threads get permits is not guaranteed by default.

The important point is that Semaphore controls the number of threads that can hold permits at the same time.

7. Why Should We Use finally to Release a Permit?

Imagine that a thread acquires a permit but encounters an exception before releasing it.

If we do not release the permit, it may remain unavailable. Over time, other threads could end up waiting unnecessarily.

That is why we place the resource-related work inside a try block and release the permit inside finally.

semaphore.acquire();

try {
    // Use the resource

} finally {
    semaphore.release();
}

Remember that acquire() must succeed before we release the permit. Otherwise, we might accidentally increase the number of available permits.

8. Fairness in Semaphore

By default, Semaphore does not guarantee that waiting threads will get permits in the order they arrived.

Sometimes, we may want threads to get permits in a fair first-come, first-served order.

Java allows us to create a fair Semaphore by passing true as the second argument.

Semaphore semaphore = new Semaphore(2, true);

Here:

  • 2 represents the number of permits.
  • true enables fairness.

With fairness enabled, waiting threads are generally granted permits in the order they requested them.

Fairness controls how waiting threads are selected. It does not guarantee that threads will finish their work in the same order.

9. When Should We Use Semaphore?

Semaphore is useful whenever we want to limit the number of threads accessing a resource simultaneously.

Some practical examples include:

  • Limiting the number of users accessing a limited resource.
  • Controlling how many threads can process requests at once.
  • Limiting concurrent access to a database connection pool.
  • Managing access to a fixed number of devices or connections.

Think of Semaphore as a system of entry passes. A thread needs a permit to enter, and it returns the permit when it is finished.

10. Important Things to Remember

  • Semaphore belongs to the java.util.concurrent package.
  • A Semaphore manages a number of permits.
  • acquire() obtains a permit and waits if none is available.
  • release() makes a permit available again.
  • The number of permits controls how many threads can acquire permits simultaneously.
  • A Semaphore can be created with fair or non-fair ordering.

Also remember that Semaphore does not automatically know which resource a thread is accessing. We use acquire() and release() around the code where we want to control access.

Test Your Understanding

Let's see how well you understood this class!

Question 1: What is the primary purpose of Semaphore?

Question 2: What does acquire() do?

Question 3: What does release() do?

Question 4: What happens when we create new Semaphore(2)?

Question 5: Why should we release a permit inside a finally block?

Class Summary

Today, we learned how Semaphore controls access to a shared resource using permits.

  • Semaphore manages a fixed number of permits.
  • acquire() obtains a permit.
  • release() returns a permit.
  • Threads wait when no permits are available.
  • Fairness can help manage the order in which waiting threads receive permits.
  • finally helps ensure that acquired permits are released.

The key takeaway: Semaphore helps us control how many threads can access a resource at the same time.

Congratulations! Part 4 Completed ๐ŸŽ‰

Communication and Coordination Between Threads

You have successfully completed Part 4 of our Java Multithreading course!

In this part, we learned how threads communicate, wait for one another, coordinate their work, and manage access to shared resources.

We explored:

  • Communication using wait(), notify() and notifyAll().
  • The difference between sleep() and wait().
  • The Producer-Consumer problem.
  • Using BlockingQueue for Producer-Consumer communication.
  • Coordinating threads with CountDownLatch.
  • Coordinating threads with CyclicBarrier.
  • Limiting access to resources with Semaphore.

These concepts help us understand how multiple threads can work together safely and efficiently.

Coming Up Next: Part 5

Managing Threads with the Executor Framework

So far, we have created and managed threads manually using the Thread class.

But what happens when an application needs to execute hundreds or thousands of tasks? Creating and managing every thread ourselves can become difficult.

In the next part, we will explore Java's Executor Framework and learn how it makes thread and task management much easier.

Let's keep learning, one concept at a time!

See you in Class 26!

0% Completed

You have completed 25 out of 46 classes.