Home/Learn/Operating Systems/Mutex vs Semaphore

Mutex vs Semaphore

Intermediate
Synchronization

A mutex is a binary lock with ownership used for mutual exclusion; a semaphore is an integer counter without ownership used for signalling and resource counting.

Overview

Both mutexes and semaphores are synchronization primitives, but they have different semantics and use cases. A mutex (mutual exclusion lock) is binary (locked/unlocked), has ownership (only the thread that locked it can unlock it), and is used to protect critical sections — exactly one thread in at a time. A counting semaphore maintains an integer counter; wait() (P/acquire) decrements it (blocking if zero) and signal() (V/release) increments it. Crucially, any thread can signal a semaphore regardless of who acquired it. A binary semaphore (count initialized to 1) looks like a mutex but lacks ownership. Mutexes are for mutual exclusion; semaphores are for signalling and resource counting. Java provides ReentrantLock as its mutex (with tryLock, fair ordering, and condition variables) and java.util.concurrent.Semaphore for counting use cases like connection pool limiting.

Mutex (ReentrantLock): Ownership and Reentrancy

Java's ReentrantLock is a mutex: it is owned by the thread that calls lock(). Only that same thread can unlock it. Reentrancy means the owning thread can lock again without deadlocking (lock count incremented). This matches the semantics of synchronized blocks. ReentrantLock adds tryLock() (non-blocking attempt), lockInterruptibly() (cancellable wait), and timed lock acquisition.

Java — ReentrantLock as mutex with tryLock and finally-unlock pattern
// ReentrantLock as mutex — only the locker can unlock
ReentrantLock mutex = new ReentrantLock();

// Thread-safe balance transfer
class BankAccount {
    private final ReentrantLock lock = new ReentrantLock();
    private long balance;

    BankAccount(long initial) { this.balance = initial; }

    public void transfer(BankAccount to, long amount) throws InterruptedException {
        // tryLock with timeout — avoids indefinite blocking
        if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
            try {
                if (balance >= amount) {
                    balance -= amount;
                    to.deposit(amount);
                    System.out.printf("Transferred %d — balance: %d%n", amount, balance);
                }
            } finally {
                lock.unlock();  // ALWAYS unlock in finally
            }
        } else {
            System.out.println("Could not acquire lock — skipping transfer");
        }
    }

    public synchronized void deposit(long amount) { balance += amount; }
    public long getBalance() { return balance; }
}

BankAccount alice = new BankAccount(1000);
BankAccount bob   = new BankAccount(500);
alice.transfer(bob, 200);
System.out.println("Alice: " + alice.getBalance() + "  Bob: " + bob.getBalance());

Semaphore: Signalling and Resource Counting

A counting semaphore controls access to a pool of N identical resources. Initialize it to N (permits available). Each acquire() takes a permit (blocks if 0); each release() returns a permit. The key difference from a mutex: any thread can release(), regardless of who acquired. This enables producer-consumer signalling. Java's Semaphore(fair=true) ensures FIFO ordering of waiting threads.

Java — Semaphore as connection pool limiter with fair ordering
// Semaphore for connection pool — limit concurrent DB connections
int MAX_CONNECTIONS = 5;
Semaphore pool = new Semaphore(MAX_CONNECTIONS, true); // fair=true: FIFO ordering

// Simulate 20 threads competing for 5 DB connections
ExecutorService exec = Executors.newFixedThreadPool(20);
AtomicInteger active = new AtomicInteger(0);

for (int i = 0; i < 20; i++) {
    int taskId = i;
    exec.submit(() -> {
        try {
            pool.acquire();  // blocks if 5 connections already in use
            int concurrent = active.incrementAndGet();
            System.out.printf("Task %2d acquired — concurrent connections: %d%n",
                taskId, concurrent);
            Thread.sleep(100); // simulate DB query
            active.decrementAndGet();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            pool.release();  // ANY thread can release — no ownership
        }
    });
}
exec.shutdown();
exec.awaitTermination(10, TimeUnit.SECONDS);

// Key: concurrent connections never exceeds MAX_CONNECTIONS
// Semaphore vs Mutex:
// Mutex:     binary, owned, mutual exclusion
// Semaphore: counter, unowned, signalling + resource counting

Key Points to Remember

  • 1Mutex: binary lock with ownership — only the locking thread can unlock; used for mutual exclusion.
  • 2Semaphore: integer counter without ownership — any thread can signal; used for resource counting and signalling.
  • 3ReentrantLock is Java's mutex — supports tryLock, timed lock, and fair ordering.
  • 4Java Semaphore(n) controls access to n identical resources — blocks when permits reach 0.
  • 5Binary semaphore (init=1) resembles a mutex but lacks ownership — a different thread can signal.
  • 6Always release semaphores and unlock mutexes in a finally block to prevent deadlock on exception.

Interview Questions

Sign in to ask Aria
1

What is the key difference between a mutex and a semaphore?

EasyAmazon
2

Can a thread that did not acquire a semaphore release it? Can it do the same with a mutex?

MediumGoogle
3

How would you implement a connection pool using Java's Semaphore?

MediumUber
4

What is a binary semaphore and why is it not equivalent to a mutex?

MediumMicrosoft

Ask Aria about Mutex vs Semaphore

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…