Semaphores come in two broad conceptual forms:
- binary
- counting
The difference is simple on paper:
- binary semaphore: at most one permit
- counting semaphore: several permits
In practice, the more important distinction is what developers infer incorrectly from the binary case.
A binary semaphore may allow only one concurrent entrant, but it is still not the same thing as a lock.
Binary Semaphore
A binary semaphore has one permit.
That means:
- one thread can acquire it
- everyone else waits until it is released
This sounds similar to a mutex. But semaphores do not express ownership in the same way locks do.
That difference affects how you reason about correctness.
Counting Semaphore
A counting semaphore has N permits.
That means:
- up to N concurrent holders may proceed
- callers beyond N must wait or fail to acquire
This is the common form used for:
- bounded connection access
- concurrency caps
- slot management
The counting case is the more obviously “permit-oriented” use.
Why Binary Semaphore Is Not Just a Lock
This is the key conceptual warning.
A lock usually implies:
- ownership by the acquiring thread
- structured critical-section discipline
- unlock by the owning thread
A semaphore does not encode the same ownership contract. It models permit availability.
That means a binary semaphore can coordinate one-at-a-time access, but it should not automatically replace a lock when the real need is:
- protecting a multi-field invariant
- expressing reentrant ownership
- using
Condition - relying on lock semantics in debugging and reasoning
Runnable Comparison Example
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class BinaryVsCountingSemaphoreDemo {
public static void main(String[] args) throws Exception {
BinaryRefreshGate refreshGate = new BinaryRefreshGate();
refreshGate.refresh();
LimitedUploader uploader = new LimitedUploader(3);
uploader.upload("file-a");
uploader.upload("file-b");
uploader.upload("file-c");
OwnershipProtectedState protectedState = new OwnershipProtectedState();
protectedState.increment();
System.out.println("Protected count = " + protectedState.value());
}
// One-at-a-time admission for a task that should not overlap.
static final class BinaryRefreshGate {
private final Semaphore gate = new Semaphore(1);
void refresh() throws InterruptedException {
gate.acquire();
try {
System.out.println("Refreshing shared snapshot");
TimeUnit.MILLISECONDS.sleep(100);
} finally {
gate.release();
}
}
}
// Bounded concurrency over several slots.
static final class LimitedUploader {
private final Semaphore slots;
LimitedUploader(int maxConcurrentUploads) {
this.slots = new Semaphore(maxConcurrentUploads);
}
void upload(String fileName) throws InterruptedException {
slots.acquire();
try {
System.out.println("Uploading " + fileName);
} finally {
slots.release();
}
}
}
// Real invariant protection is still often clearer with a lock.
static final class OwnershipProtectedState {
private final ReentrantLock lock = new ReentrantLock();
private int value;
void increment() {
lock.lock();
try {
value++;
} finally {
lock.unlock();
}
}
int value() {
lock.lock();
try {
return value;
} finally {
lock.unlock();
}
}
}
}
The point of the example is not syntax. It is boundary selection:
- binary semaphore for one-at-a-time admission
- counting semaphore for bounded parallelism
- lock for invariant ownership
Good Use Cases
Binary semaphore fits when:
- one operation should not overlap with itself
- the problem is one-at-a-time admission, not rich lock semantics
- you want a permit model, not a critical-section API
Counting semaphore fits when:
- there are multiple equivalent slots
- the system can safely run several tasks in parallel but only up to a limit
Examples:
- binary: allow only one cache rebuild at a time
- counting: allow at most 20 concurrent exports
Common Mistakes
Treating a binary semaphore as a reentrant lock
It is not reentrant. If the same thread acquires it twice without release, it blocks itself.
Using a semaphore to protect complex invariants
You can sometimes force the design, but the code often becomes harder to understand than a simple lock-based critical section.
Ignoring ownership semantics
If the logic relies on the concept that only the owner should release, a lock is usually the safer and clearer abstraction.
Decision Guide
Choose a binary semaphore when:
- the system concept is “only one active admission at a time”
- you do not need lock features such as reentrancy or conditions
Choose a counting semaphore when:
- the system concept is “there are N interchangeable capacity units”
Choose a lock when:
- the real problem is protected state ownership and invariant safety
Key Takeaways
- A binary semaphore allows only one concurrent permit holder; a counting semaphore allows several.
- Binary semaphores can look like mutexes, but they do not communicate the same ownership model as locks.
- Counting semaphores are natural for bounded-capacity access.
- If the real requirement is state protection rather than admission control, a lock is usually clearer.
Next post: Rate Limiting and Concurrency Limiting with Semaphore
Comments