Java Semaphore: Complete Guide with Real-World Examples, Code, and Interview Questions
Semaphore is one of the most useful concurrency utilities in Java when you want to limit the number of threads that can access a resource at the sameβ¦
Semaphore is one of the most useful concurrency utilities in Java when you want to limit the number of threads that can access a resource at the same time.
What is Semaphore?
A Semaphore controls access to a shared resource by maintaining a fixed number of permits.
- If a permit is available β thread proceeds.
- If no permit is available β thread waits.
- When work finishes β permit is released.
- Another waiting thread gets permission.
Think of it as a parking lot.
Parking Lot Capacity = 20
Car arrives
β
βΌ
Space Available?
β
βββββ΄βββββ
β β
Yes No
β β
Park Wait
β
Leave
β
Space becomes free
β
Next waiting car enters
Real-world Example
Suppose your e-commerce application uses Razorpay/Stripe.
The payment provider allows only 20 concurrent payment requests.
If your application suddenly receives 100 payment requests, sending all 100 together may:
- exceed API limits
- cause throttling
- receive HTTP 429 (Too Many Requests)
- slow down payment gateway
Instead, you process only 20 simultaneously.
100 Payment Requests
β
βΌ
Semaphore (20 permits)
βββββββββββββββββββββββββββ
β 20 Requests Processing β
βββββββββββββββββββββββββββ
80 Waiting
β
βΌ
One payment completes
Permit released
β
βΌ
Next waiting request starts
Only 20 are active at any time.
Without Semaphore
Imagine 100 threads.
100 Threads
Thread1
Thread2
Thread3
...
Thread100
β
All hit Payment Gateway
β
Gateway Overloaded
β
Some requests fail
With Semaphore
Semaphore = 20
100 Threads
β
20 acquire permit
β
20 processing
β
80 blocked
β
One finishes
β
release()
β
One waiting thread starts
Concurrency is controlled automatically.
Basic Methods
Semaphore semaphore = new Semaphore(20);
Meaning:
20 permits available
acquire()
Takes one permit.
semaphore.acquire();
If permit exists
Permits = 20
Thread A
β
Acquire
β
Permits = 19
If no permit exists
Permits = 0
Thread arrives
β
Wait
The thread blocks until someone releases a permit.
release()
Returns permit.
semaphore.release();
Permits = 0
Thread finishes
β
release()
β
Permits = 1
β
Waiting thread wakes up
Example Code
import java.util.concurrent.*;
public class PaymentGatewayExample {
private static final Semaphore semaphore = new Semaphore(3);
public static void main(String[] args) {
ExecutorService executor =
Executors.newFixedThreadPool(10);
for (int i = 1; i <= 10; i++) {
int paymentId = i;
executor.submit(() -> processPayment(paymentId));
}
executor.shutdown();
}
private static void processPayment(int paymentId) {
try {
semaphore.acquire();
System.out.println(
"Payment " + paymentId +
" started by " +
Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println(
"Payment " + paymentId +
" completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release();
}
}
}
Output
Payment 1 started
Payment 2 started
Payment 3 started
(Only these three)
After 3 seconds
Payment 1 completed
Payment 4 started
Payment 2 completed
Payment 5 started
...
Notice
Even though
10 threads exist
only
3 payments
run simultaneously.
Execution Flow
Initially
Permits = 3
Thread 1
Acquire
Permits = 2
Thread 2
Acquire
Permits = 1
Thread 3
Acquire
Permits = 0
Thread 4
Acquire
β
No permit
β
WAITING
Thread 5
WAITING
Thread 1 finishes
release()
Permits = 1
β
Thread4 wakes up
Acquire
Permits = 0
Again only 3 threads execute.
Why release() should always be inside finally
Suppose payment processing throws an exception.
Wrong
semaphore.acquire();
processPayment();
semaphore.release();
If
processPayment()
throws exception,
release()
never executes.
Permit is permanently lost.
Eventually
Permits = 0
Every thread waits forever.
Correct
try {
semaphore.acquire();
processPayment();
} finally {
semaphore.release();
}
Now permit is always returned.
Real-world Spring Boot Example
Imagine your application receives 1000 orders per second.
Each order needs to call an external fraud detection API.
Order Service
β
Fraud Detection API
β
External Vendor
Vendor documentation says:
Maximum Concurrent Requests = 50
Implementation
@Service
public class FraudService {
private final Semaphore semaphore =
new Semaphore(50);
public FraudResult checkFraud(Order order)
throws InterruptedException {
semaphore.acquire();
try {
return fraudClient.check(order);
} finally {
semaphore.release();
}
}
}
No matter how many requests reach your application,
only
50
external API calls
run simultaneously.
Another Example β Database Connection Pool
Suppose
Database Connections = 10
Application receives
200 requests
Only
10
can use database
simultaneously.
Remaining requests wait until a connection is released.
Many connection pool implementations internally use semaphore-like mechanisms to control access.
Another Example β File Upload Server
Suppose image compression is CPU intensive.
You allow only
5
compressions
at a time.
100 uploads
β
Semaphore(5)
β
5 compressing
β
95 waiting
This prevents CPU exhaustion.
Semaphore vs CountDownLatch
| Feature | Semaphore | CountDownLatch |
|---|---|---|
| Purpose | Limit concurrent access | Wait until tasks finish |
| Threads wait for | A permit | Latch count becomes zero |
| Reusable | β Yes | β No |
| Controls concurrency | β Yes | β No |
| Blocks worker threads? | Yes, when no permits are available | Worker threads usually don’t block; the waiting thread calls await() |
| Typical use | Rate limiting, connection pools, API throttling | Wait for multiple tasks to complete |
Semaphore vs CyclicBarrier
| Feature | Semaphore | CyclicBarrier |
|---|---|---|
| Purpose | Limit concurrent threads | Synchronize phases |
| Waiting Condition | Permit unavailable | All participating threads reach the barrier |
| Reusable | β Yes | β Yes |
| Controls concurrency | β Yes | β No |
| Typical Use | External API limits, DB connections | Multi-phase parallel algorithms |
When should you use Semaphore?
Use a Semaphore when you need to restrict how many threads can access a shared resource at the same time, such as:
- β Payment gateway with a concurrency limit
- β Third-party REST APIs with rate/concurrency restrictions
- β Database connection pools
- β File upload or image/video processing
- β Email or SMS sending services
- β Printer access (limited printers)
- β GPU-intensive or CPU-intensive tasks
- β Any limited-capacity shared resource
Interview Definition
Semaphore is a synchronization utility in Java that controls access to a shared resource by maintaining a fixed number of permits. A thread must acquire a permit before accessing the resource and release it after completion. If no permits are available, additional threads wait until a permit is released. It is commonly used to limit concurrent access to resources such as database connections, payment gateways, and third-party APIs.
Some important interview questions on Semaphore
1. What is a Semaphore in Java?
Answer
A Semaphore is a synchronization utility in Java that controls access to a shared resource using a fixed number of permits.
- A thread must acquire a permit before accessing the resource.
- After completing its work, it releases the permit.
- If no permits are available, the thread waits until one is released.
2. Why do we use Semaphore?
Answer
Semaphore is used to limit the number of threads that can access a resource simultaneously.
Examples:
- Payment Gateway
- Database Connections
- Third-party APIs
- Printer access
- File uploads
- Rate limiting
3. What happens if all permits are occupied?
Example:
Semaphore semaphore = new Semaphore(2);
Thread 1
acquire()
Thread 2
acquire()
Thread 3
acquire()
Answer
Thread 3 blocks until one of the first two threads calls release().
4. What is the difference between acquire() and release()?
Answer
acquire()
- Takes one permit.
- Waits if no permit is available.
release()
- Returns the permit.
- Wakes one waiting thread (if any).
5. Why should release() be inside finally?
Answer
Because exceptions may occur.
Wrong
semaphore.acquire();
process();
semaphore.release();
If process() throws an exception,
release()
never executes.
Eventually,
All permits are consumed.
Every thread waits forever.
Correct
try {
semaphore.acquire();
process();
} finally {
semaphore.release();
}
6. Can Semaphore have only one permit?
Yes.
Semaphore semaphore = new Semaphore(1);
This behaves similarly to a mutex (mutual exclusion).
Only one thread can access the resource at a time.
7. What is the difference between Semaphore(1) and synchronized?
| Semaphore(1) | synchronized |
|---|---|
| Explicit lock management | JVM-managed monitor lock |
| Can acquire/release in different methods or even different threads (though unusual and error-prone) | Lock must be exited by the same thread that entered the synchronized block |
Supports timeout (tryAcquire) | No timeout |
| Supports multiple permits | Always one thread |
| Can be fair | No fairness guarantee |
8. What is a Fair Semaphore?
Semaphore semaphore =
new Semaphore(3, true);
The second parameter
true
means
FIFO order
Threads receive permits in arrival order.
Without fairness
Semaphore semaphore =
new Semaphore(3);
Java may allow newer threads to acquire permits before older waiting threads.
9. What is tryAcquire()?
Instead of waiting,
if (semaphore.tryAcquire()) {
// process
} else {
// reject request
}
If no permit exists,
returns false
instead of blocking.
Useful for
- API throttling
- Payment services
- Fast failure
10. What is tryAcquire(timeout)?
semaphore.tryAcquire(5, TimeUnit.SECONDS);
The thread waits for up to 5 seconds.
If no permit becomes available,
returns false
11. What happens if release() is called more times than acquire()?
Example
Semaphore semaphore = new Semaphore(2);
semaphore.release();
Now permits become
3
Answer
Semaphore does not enforce ownership. Calling release() without a matching acquire() increases the permit count, which can lead to bugs if misused.
12. Is Semaphore reentrant?
No.
Unlike a ReentrantLock, a Semaphore does not track which thread owns a permit.
A thread can call acquire() twice, consuming two permits if available.
13. Is Semaphore thread-safe?
Yes.
Semaphore internally uses Java’s synchronization mechanisms and is safe to use across multiple threads.
14. Can Semaphore be reused?
Yes.
Unlike CountDownLatch, Semaphore can be used repeatedly.
Example
Acquire
β
Release
β
Acquire again
β
Release again
15. Difference between Semaphore and CountDownLatch?
| Semaphore | CountDownLatch |
|---|---|
| Controls concurrent access | Waits for tasks to finish |
| Uses permits | Uses a countdown |
| Reusable | One-time use |
| Threads acquire permits | One or more threads wait on await() until the count reaches zero |
16. Difference between Semaphore and CyclicBarrier?
| Semaphore | CyclicBarrier |
|---|---|
| Limits concurrent access | Synchronizes phases |
| Uses permits | Waits until all participating threads arrive |
| Threads don’t wait for each other unless permits are exhausted | Every participating thread waits at the barrier |
| Used for throttling | Used for phased computation |
17. Can Semaphore be used for rate limiting?
Yes.
Example
API allows
20 concurrent requests.
Use
Semaphore semaphore =
new Semaphore(20);
Only 20 requests execute simultaneously.
18. Where is Semaphore used in Spring Boot?
Common examples:
- Payment Gateway Integration
- Email Service
- SMS Service
- AWS S3 Upload
- File Compression
- External REST APIs
- Fraud Detection APIs
- OCR Services
- AI Model Inference
19. What happens if a thread is interrupted while waiting?
semaphore.acquire();
throws
InterruptedException
Always handle it properly:
try {
semaphore.acquire();
// work
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore interrupt status
} finally {
// Only release if a permit was actually acquired
}
A common pattern is to track acquisition:
boolean acquired = false;
try {
semaphore.acquire();
acquired = true;
// work
} finally {
if (acquired) {
semaphore.release();
}
}
20. Give a real-world use case for Semaphore.
Payment Gateway
Gateway Limit
β
20 concurrent requests
β
Application receives
100 requests
β
Semaphore(20)
β
20 processing
80 waiting
β
One finishes
β
Next starts
This protects the payment gateway from being overloaded.
21. Does Semaphore create threads?
No.
Semaphore does not create or manage threads.
It only controls how many existing threads can access a resource at the same time.
Usually it is used together with:
ExecutorServiceThreadPoolExecutorForkJoinPool- Spring Boot
@Async
22. Does Semaphore reduce the number of threads?
No.
Suppose
ExecutorService = 100 threads
Semaphore = 20 permits
There are still 100 threads in the pool.
However,
20 threads
β
Working
80 threads
β
Blocked waiting for permit
Semaphore limits concurrent access, not the size of the thread pool.
23. Can Semaphore replace ExecutorService?
No.
Their responsibilities are different:
- ExecutorService manages the lifecycle and scheduling of threads.
- Semaphore controls how many of those threads may access a particular resource concurrently.
They are often used together.
24. Which concurrency utility would you choose in these scenarios?
| Scenario | Utility |
|---|---|
| Wait until all tasks complete | CountDownLatch |
| All threads must finish phase 1 before phase 2 | CyclicBarrier |
| Only 20 payment requests at a time | Semaphore |
| Execute asynchronous tasks | ExecutorService / CompletableFuture |
| Lock a critical section for one thread | synchronized or ReentrantLock |
Interview Tip
A question that interviewers frequently ask is:
“If you’re already using a thread pool of 20 threads, why would you still need a Semaphore with 5 permits?”
A strong answer is:
A thread pool limits the number of worker threads, while a Semaphore limits access to a specific shared resource. A thread may perform many operations, but only one of them (for example, calling a third-party payment API) needs concurrency control. With a 20-thread pool and a
Semaphore(5), all 20 threads can continue doing other work, but only 5 are allowed to call the payment service at the same time. This separates thread management from resource access control.