Mutex vs semaphore
Compare mutex and semaphore behaviour in Java and Python. Change who releases to inspect owner checks, repeated releases and permit counts.
A mutex lets one thread at a time enter a protected section; a semaphore counts permits or signals readiness. Ownership depends on the API: Java ReentrantLock requires its owner to unlock, Python threading.Lock does not, and Java Semaphore records no owner.
Simplified example: A takes the only permission and B tries to return it. An owner check refuses B. Java's semaphore accepts B's return; returning it again raises the count to two. Python's primitive Lock accepts B's first release but rejects another while it is unlocked.
Try one acquisition followed by two releases: A acquires, B tries to release, then A tries to release. Watch what remains after each action. Changing the selected thread does not create a fresh acquisition, and a successful release does not mean the next release will also succeed.
Try a release from another thread#
A and B are illustrative threads inside one process. They can reach the same shared data; Process vs thread explains that memory boundary. Select an API below, predict B’s result, then switch to A without resetting. A new primitive or Reset acquisition starts the example again.
Change who releases the acquired primitive
Illustrative example: A acquires once. Predict what happens when B releases. Select a specific contract: the name “lock” alone does not settle ownership. Changing the releasing thread keeps the current state; changing the primitive or choosing Reset acquisition starts a fresh acquisition by A.
Current state: Java ReentrantLock — owner: A; hold count: 1.
Java ReentrantLock: A owns the lock; B's unlock throws IllegalMonitorStateException and leaves A's hold intact. Java Semaphore and Python's primitive Lock permit release from B.
Single acquisition, no contention or reentrancy. A Java Semaphore release increments its count and does not verify prior acquisition; over-release can exceed the intended capacity. This does not model a POSIX undefined-behaviour outcome.
Java 21 ReentrantLock records an owner and a hold count. B’s unlock() throws IllegalMonitorStateException, leaving A’s hold intact. A can then unlock its own hold. With no hold left, another unlock fails.
Python 3.14 threading.Lock has a locked or unlocked state without a thread owner. B can release A’s acquisition. The lock is then unlocked, so a second release by either A or B raises RuntimeError. Python’s RLock is different: it tracks an owner and requires that thread to release.
Java 21 Semaphore starts this example with one available permit. A’s acquisition reduces the count to zero. B’s release raises it to one; A’s later release raises it to two. The API checks neither which thread acquired nor whether a release has a matching acquisition. That second release exceeds the intended capacity of this example.
A rejected owner check and undefined behaviour are different contracts. C++ std::mutex requires the unlocking thread to own it. The POSIX mutex table makes a non-owner unlock undefined for a non-robust POSIX normal mutex. A robust POSIX normal mutex, or a POSIX error-checking or recursive mutex, reports an error. Undefined behaviour promises neither success nor a particular failure. The lab models the named Java/Python outcomes, not an invented outcome for those C++ or POSIX cases.
Ownership also does not imply recursive acquisition. Java ReentrantLock lets its owner acquire again and requires matching unlocks; Java’s synchronized monitors also allow re-entry. C++ uses a separate std::recursive_mutex; relocking an owned std::mutex violates its contract. POSIX selects PTHREAD_MUTEX_RECURSIVE through mutex attributes. These features are outside the lab’s single acquisition.
Priority inheritance is another separate choice. A high-priority thread can wait for a lock held by a low-priority thread while a medium-priority thread runs. A POSIX mutex configured with PTHREAD_PRIO_INHERIT lets its holder inherit higher-priority waiters’ priority while holding the relevant mutexes. Merely choosing a mutex does not enable that protocol.
One permit needs a matching release#
A semaphore can provide mutual exclusion: only one thread enters the protected section at a time. Start with one permit, acquire before entering and release after leaving. Keep the available count at zero or one. Java’s documentation calls this a binary semaphore when it is used that way; initialising its count to one alone does not preserve the limit.
The extra release in the experiment breaks that intended limit. It is legal for Java Semaphore to accept a release from a thread that never acquired, but it can still be wrong for the application’s resource accounting. Each successful acquisition needs exactly one release after its protected work. An owner check and a capacity rule answer different questions.
Check other semaphore APIs separately. C++ counting_semaphore permits acquisition and release in different threads, but its release preconditions limit the resulting count. Python Semaphore counts permits without ownership; BoundedSemaphore additionally rejects a release that would exceed its initial value. Do not transfer Java’s capacity behaviour to every semaphore implementation.
Choose a signal or a protected condition#
For an illustrative readiness signal, create a Java Semaphore with zero permits. The consumer’s acquisition waits. When the producer finishes setup, it releases one permit so the consumer can proceed. Producer and consumer do not need to be the same thread. A ReentrantLock acquired by the consumer cannot support that handoff through a producer unlock: the producer does not own it.
A shared condition needs a different pattern. If a consumer must wait until a queue is non-empty, use a condition variable with the lock that protects the queue. Check the queue while holding that lock, wait while it is empty, then check again after waking. Java’s Condition.await() releases the associated lock while waiting and reacquires it before returning. A wakeup can be spurious, and another consumer may have taken the item, so the check belongs in a loop.
Review which thread acquires and releases, and what state the waiting thread needs to observe. Calls in different functions do not by themselves violate ownership; calls from different threads do for ReentrantLock. Keep the protected work inside the intended acquisition and release, including error paths, rather than relying on a particular schedule to hide a wrong-owner call.
These local Java and Python objects do not coordinate writers on another machine. The distributed-systems path follows coordination across network failures and retries. Within a process, correct release rules also do not prevent every cycle of waiting: Deadlocks, seen demonstrates that separate problem with two database transactions.
| Situation | Take | Because |
|---|---|---|
| One thread at a time may touch this data | Lock / mutex | Protect the shared data; check the API’s release contract |
| At most N may use a shared resource | Semaphore | Pair acquisitions and releases to preserve the intended capacity |
| One thread signals another that work is ready | Semaphore | Release from a different thread is legal |
| The lock and unlock bracket one function | Lock / mutex | Release reliably on every exit, including errors |
- Situation
- One thread at a time may touch this data
- Take
- Lock / mutex
- Because
- Protect the shared data; check the API’s release contract
- Situation
- At most N may use a shared resource
- Take
- Semaphore
- Because
- Pair acquisitions and releases to preserve the intended capacity
- Situation
- One thread signals another that work is ready
- Take
- Semaphore
- Because
- Release from a different thread is legal
- Situation
- The lock and unlock bracket one function
- Take
- Lock / mutex
- Because
- Release reliably on every exit, including errors
IF YOU REMEMBER ONE THING
Follow one acquisition through every release. Check who may release, what state remains and whether extra releases exceed the intended capacity. Java ReentrantLock, Java Semaphore and Python’s primitive Lock answer those questions differently.
Questions people also ask
5 QUESTIONSIs a mutex just a semaphore set to one?
A semaphore can provide exclusion while its available count stays at most one and code acquires before entering, then releases after leaving, the protected section. Java Semaphore does not enforce that maximum or match releases to acquisitions: an extra release can raise its count. It also has no thread-owner check.
Can another thread unlock a mutex?
Java ReentrantLock rejects a non-owner with IllegalMonitorStateException. C++ std::mutex and a non-robust POSIX normal mutex make that misuse undefined behaviour. A robust POSIX normal mutex, or a POSIX error-checking or recursive mutex, reports an error. Python threading.Lock permits release by another thread; threading.RLock requires its owner.
What is a binary semaphore for?
A binary semaphore keeps its available count at zero or one. It can provide exclusion or carry a readiness signal. In a signalling example, start at zero: one thread releases when work is ready and another acquires before proceeding. Java Semaphore does not impose a maximum of one, so the application must preserve that limit.
What is priority inversion?
A high-priority thread waits for a lock held by a low-priority thread while a medium-priority thread runs. POSIX's configured PTHREAD_PRIO_INHERIT protocol lets a holder inherit the priority of higher-priority waiters while it holds the relevant mutexes. Priority inheritance is a separate feature, not a guarantee of every mutex.
When should I use a condition variable instead?
Use a condition variable to wait for shared state, such as a queue becoming non-empty. Check and change that state while holding the associated lock. Java Condition.await releases that lock and reacquires it before returning; recheck the condition in a loop because a wakeup does not guarantee it is still true.