
Techniques to handle deadlocks:
→ in PProg, design algorithm to not allow deadlocks:
- via ressource ordering
- may be “always acquire object with smaller id’s lock first” (see bank account example)
- or fixed ordering “always hashtable, then map”
- two phase locking with retry
10.1 Semaphores
Why do we need more than locks?
- communication: locks don’t provide a way for threads to communicate about changes in state (“data ready”) → so we might acquire lock on a queue in a prod/consumer pattern to find the queue empty
- ordering: locks don’t enforce a specific order for waiting threads
- ressource counting: either lock is held or not → no count of how many
Ownership
A semaphore has no notion of ownership → can
releaseeven if I didn’t callacquire!
Semantics:
- A semaphore S is an abstract data type holding a non-negative integer value, initialized to some value .
- It supports two atomic operations:
acquire(S)(orP(S),wait(S))release(S)(orV(S),signal(S))
// Acquire:
// ---- Start Atomic ----
wait until S > 0; // Block if S is zero
S = S - 1;
// ---- End Atomic ----
// Release:
// ---- Start Atomic ----
S = S + 1;
// ---- End Atomic ----Intuition: acquire tries to decrement the count, waiting if it’s zero. release increments the count (potentially waking up a waiting thread).
Semaphore > Lock We can reduce a semaphore to a standard mutex (“binary semaphore”).
- we initialise with 1 (“unlocked”)
lock()decrements to → now lockedunlock()increments to → if another thread was waiting, it can now proceed
Semaphore implementation:
- usually on OS level or language runtime level
- blocking queue
- no spinnning → we just update on queue ops
10.1.1 Rendezvouz
Assume computation (ex: dot-product) in two phases, that requires all threads to wait to complete before proceeding (to addition in the dot-product).
If we implement this wrong, it can lead to a deadlock:

To avoid that, we need to separate the signalling and waiting → first signal, then wait on the other:

10.2 Barriers
Barrier: A synchronization primitive used to make a number of threads wait until all of them have reached a particular point in the code before any of them are allowed to proceed.
10.2.1 Barrier with turnstile
important:
count++;not atomicif (count == n) relase();and all pass throughacquire()- only one thread actually released → others still stuck
if we fix both:
- turnstile:
acquire(); realease();pattern → works, but issue for re-use- the last thread called
release→ so on next loop it’s already wide open
- the last thread called
10.2.2 Reusable barrier attempt
to fix the “last thread left turnstile open” problem, we could add a “reset phase”:
acquire(mutex); count--; release(mutex);
if (count == 0) acquire(barrier);→ problem: fast thread might have already run around to complete phase 1 again because we let all threads through phase 2 without “rendezvouzing” again.
10.2.3 Two-Phase Reusable Barrier
Two phases and two barriers:
- ensure all threads have finished before any can start phase 2
- only
release(barrier1)when are stuck waiting for it - in the same move
acquire(barrier2)to block that
- only
- all threads must finish phase 2 before any can start new phase 1
release(barrier2)when have passed second phase- in the same move,
acquire(barrier1)to “relock phase 1” for the next round
the acquire(barrier1); release(barrier1); line is like a turnstile only one thread can go through at a time
- the acquire blocks all until the -th releases
- then they all filter through

10.3 Producer-Consumer Pattern
We can use a bounded (circular) FIFO queue to implement this pattern. Producer enqueues, consumer dequeues.
10.3.1 Basic Spinning Implementation
How do we differentiate between full and empty queue?

We can leave an empty space → no need for a counter variable that needs to be atomic!
Problem:
// Inside Queue class...
public synchronized void enqueue(long item) {
while (isFull()) {
; // Spin-wait (BAD!) - holds the lock!
}
doEnqueue(item);
}
public synchronized long dequeue() {
while (isEmpty()) {
; // Spin-wait (BAD!) - holds the lock!
}
return doDequeue();
}→ DEADLOCK! if enqueue finds queue is full, it spins while holding the lock!
10.3.2 Attempt with sleep()
Kind of an unholy child of exponential back-off and busy waiting here.
// Inside Queue class... (Illustrative, enqueue only)
public void enqueue(long item) throws InterruptedException {
while (true) {
synchronized(this) { // Acquire lock
if (!isFull()) {
doEnqueue(item);
return; // Success!
}
// If full, release lock by exiting synchronized block
}
// Lock is released, sleep for a bit before retrying
Thread.sleep(timeout); // Sleep *without* holding the lock!
}
}Problems:
- no direct notification → have to wait until consumer wakes up!!!
- busy waiting / polling → inefficient
- choosing timeout is hard
- might have 10s until something happens
- if too quick → livelock maybe
10.3.3 Semaphores
Semaphore have build in counter + waiting mechanism.
Idea: We initialise one with the capacity of the queue → it does it all for us.
- not possible:
- either we have a semaphore counting free Slots (starts with
Semaphore(N))- then the producers produce (
acquire) until . - but there’s no “value = N” (empty) block that would stop consumers from consuming (
release)
- then the producers produce (
- same thing happens if we have it count
emptySlotsonly!
We always need the “baton-passing” patternnonFullandnonEmptywith two semaphores and a lock for mutex!
- either we have a semaphore counting free Slots (starts with
- Deadlock → we need to correctly implement to avoid problems!
Acquire the condition semaphore (nonFull or nonEmpty) before acquiring the mutex (manipulation).
→ thread only gets mutex when it knows it can proceed.
Why Are Semaphores (and Locks) Problematic?
While semaphores work, they are considered somewhat “unstructured”:
- Discipline Required
- Deadlock Prone
- Condition Waiting: Basic locks/semaphores don’t cleanly integrate waiting for an arbitrary condition (like isFull() == false) with mutual exclusion. We either spin (bad) or use complex semaphore arrangements.
What we need: A construct that combines mutual exclusion (like a lock) with the ability to efficiently wait for a specific condition while temporarily releasing the lock, and be notified when that condition might have become true.
10.4 Monitors
Monitors, invented by Tony Hoare and Per Brinch Hansen, provide this higher-level abstraction.
Definition: A monitor is an abstract data structure (like a class or module) where:
- Data is encapsulated.
- Operations (methods) on that data execute with implicit mutual exclusion – only one thread can be executing any method of the monitor instance at a time.
- It includes mechanisms for threads to wait for conditions and signal other threads when conditions change.
How would our queue look conceptually with a monitor?
// Monitor-based Queue (Conceptual)
public monitor class BoundedQueue {
// internal state (buffer, in, out, etc.)
public void enqueue(long item) { // Implicit mutual exclusion
while (isFull()) {
wait_until_not_full(); // Wait if condition not met
}
doEnqueue(item);
signal_that_not_empty(); // Notify waiting consumers
}
public long dequeue() { // Implicit mutual exclusion
while (isEmpty()) {
wait_until_not_empty(); // Wait if condition not met
}
long item = doDequeue();
signal_that_not_full(); // Notify waiting producers
return item;
}
}The monitor handles the locking. We just need primitives to wait for conditions and signal when conditions change.
Monitor Semantics for Condition Handling:
When a thread inside a monitor operation finds a condition doesn’t hold (e.g., isFull() is true):
- It needs to atomically release the monitor lock.
- It needs to wait until the condition potentially becomes true.
- Another thread, upon changing the state (e.g., making the queue not full), needs to signal waiting threads.
- A waiting thread, upon being signaled, needs to re-acquire the monitor lock before resuming execution.
10.4.2 Monitors in Java (synchronized, wait, notify, notifyAll)
class Queue {
// ... state variables, constructor, helpers ...
synchronized void enqueue(long x) {
while (isFull()) { // MUST use while, not if!
try {
wait(); // Releases lock on 'this', waits
} catch (InterruptedException e) { /* handle */ }
// Re-acquires lock upon waking
}
doEnqueue(x);
notifyAll(); // Notify any waiting consumers
}
synchronized long dequeue() {
while (isEmpty()) { // MUST use while, not if!
try {
wait(); // Releases lock on 'this', waits
} catch (InterruptedException e) { /* handle */ }
// Re-acquires lock upon waking
}
long item = doDequeue();
notifyAll(); // Notify any waiting producers
return item;
}
}Important Questions & Refinements:
whilevsifaroundwait():- You must use
while (condition) wait();- Why? Because a thread can wake up from
wait()for reasons other than the specific condition being true (spurious wakeups) - or another thread might have changed the condition again between the notify and the woken thread re-acquiring the lock.
- Why? Because a thread can wake up from
- The while loop ensures the condition is re-checked after waking. Using if can lead to errors.
- You must use
notify()vsnotifyAll():- Why
notifyAll()here?- If we used
notify(), we might accidentally wake up another producer when the queue becomes not full (instead of a consumer), or wake a consumer when the queue becomes not empty (instead of a producer). notifyAll()wakes everyone, and the while loop lets only the threads whose condition is actually met proceed.
- If we used
notify()can be used as an optimization only if you are certain that any thread woken can make progress and that only threads waiting for the specific signaled condition need to be woken.notifyAll()is generally safer.
- Why

10.4.3 Java Monitor Implementation Details


wait()moves thread fromRUNNABLEtoWAITING(orTIMED_WAITINGifwait(timeout)is used).notify()/notifyAll()moves waiting threads toBLOCKED- compete for re-entry
Monitor Queues each monitor has
- entry queue with threads waiting to acquire the lock (via synchronized)
- wait set (condition queue) threads that called
waitand are waiting for a notification
Notification process: whennotify/notifyAllis called → thread(s) are moved from the wait set → entry queue.
In Java: signaling and continue (signaling process continues running → signaling process moves signaled process to waiting entry queu)
- there is also signal and wait → hands lock directly to notified
- not implemented in Java like this
10.4.4 Alternative java.util.concurrent.locks.Condition
Intrinsic locks (synchronized/wait/ notify`) have limitations:
- One implicit lock per object.
wait/notifywork on a single, implicit condition set per object.- Tied to block structure.

The java.util.concurrent.locks package provides more flexibility:
- Lock interface (e.g.,
ReentrantLock) provides explicitlock()andunlock(). - A Lock object can create multiple
Conditionobjects associated with it usinglock.newCondition().

Condition Interface:
- Each Condition object represents a separate wait set associated with the same Lock.
await(): Called while holding the associated Lock.- Atomically releases the Lock and waits on this specific condition.
- Re-acquires the Lock before returning.
- Must re-check application condition in a while loop. (await stands for atomic wait)
signal()/signalAll(): Called while holding the associated Lock. Wakes up one/all thread waiting on this specific condition.
This allows creating more complex synchronization logic, like separate conditions for “not full” and “not empty” associated with the same buffer lock, potentially allowing more targeted signal() calls instead of always using signalAll().
10.4.6 Queue (Producer-Consumer) using Monitors


10.4.7 Sleeping Barber Variant
Solving Sleeping Barber:
- To avoid lost signals, the barber/consumer needs to know if customers/producers are actually waiting before going to sleep, and customers/producers need to know if the barber/consumer is actually sleeping before trying to wake them.
- This typically requires additional shared counters to track the number of waiting producers and consumers.

Explanation:
- The counters
nandmare decremented before the wait condition check.- A negative value indicates that adding/removing this item might satisfy a waiting thread on the other side.
- The counters are incremented after the operation that changes the buffer state.
- Crucially, signal is only called if the corresponding counter was less than or equal to zero before it was incremented (meaning the other type of thread was definitely blocked or about to block).
- This avoids signaling when no threads are waiting.