A counting semaphore holds a nonnegative number of permits.
down waits until a permit is available and consumes one; up returns a permit and can release a waiting caller. A binary semaphore restricts the number available to zero or one. Unlike a mutex, a semaphore need not be released by the same software thread that acquired it; this permits producer-to-consumer signalling.Interpret
maxCount as the initial number of permits and, for this bounded implementation, the maximum permitted number of unmatched releases. An up beyond that capacity is a programming error, rather than a blocking operation. Clients obey this release discipline. If an unbounded counting semaphore is desired instead, remove the upper-bound requirement and use a sufficiently wide counter; a capacity-one instance is binary only under the corresponding balanced-use discipline.class CountingSemaphore(maxCount: Int) extends Semaphore {
require(maxCount > 0)
private var count = maxCount
def down: Unit = synchronized {
while (count == 0) wait()
count -= 1
}
def up: Unit = synchronized {
require(count < maxCount, "unmatched semaphore release")
count += 1
notify()
}
}All accesses to
count occur under the same monitor. wait atomically releases that monitor while sleeping, then reacquires it before returning. The while is needed because of spurious wakeups and because another caller can consume the returned permit before the notified caller reacquires the monitor. Only down callers wait here, and they all wait on the same predicate; hence waking one with notify is sufficient. A later up notifies another waiter if necessary. This use of notify will not be safe for the mixed predicates in the next part.[ref]Java 8 Object API documentation. notify and wait: arbitrary notification choice, monitor reacquisition and spurious wakeups
A binary semaphore initially available is
new CountingSemaphore(1). To initialize it unavailable, call its down once during construction, before publishing it to other software threads. Its state obeysThis implementation provides no first-come-first-served guarantee: a new caller can acquire the monitor before a notified waiter. The following examples assume ordinary successful execution without cancellation or interruption of a partially completed buffer operation.A FIFO semaphore must reserve a released permit for the oldest waiter. Merely waking the oldest caller is insufficient if a newcomer can consume the permit first. Keep an explicit FIFO queue of private wait records, and transfer the permit directly to the oldest record. Each record has its own monitor, so releasing a permit wakes exactly its chosen recipient.
class FifoBinarySemaphore(initiallyAvailable: Boolean) extends Semaphore {
private final class Waiter { var granted = false }
private val queue = scala.collection.mutable.Queue[Waiter]()
private var available = initiallyAvailable
def down: Unit = {
val mine: Option[Waiter] = synchronized {
if (available) {
assert(queue.isEmpty)
available = false
None
} else {
val w = new Waiter
queue.enqueue(w)
Some(w)
}
}
mine.foreach { w =>
var interrupted = false
w.synchronized {
while (!w.granted) {
try { w.wait() }
catch { case _: InterruptedException => interrupted = true }
}
}
if (interrupted) Thread.currentThread().interrupt()
}
}
def up: Unit = synchronized {
if (queue.nonEmpty) {
val w = queue.dequeue()
// available stays false: this permit is reserved for w.
w.synchronized {
w.granted = true
w.notify()
}
} else {
require(!available, "unmatched binary-semaphore release")
available = true
}
}
}The wait loop is deliberately uninterruptible with interruption status restored after acquisition: abandoning a queued wait record would otherwise strand its reserved permit. A caller that acquires must still complete the buffer operation.
granted is read and written under the wait record's own monitor. The persistent flag prevents a lost wake-up if up runs after registration but before the caller begins waiting. Spurious wakeups simply repeat the loop.The outer monitor serializes registration and the FIFO queue. A caller either consumes a genuinely free permit when nobody is queued, or appends one record.
up removes exactly the front record and leaves available false, even if its owner has not yet resumed. A newcomer must queue behind existing records instead of stealing the handoff. The outer-to-inner lock ordering in up is safe because waiting callers release the outer monitor before acquiring their private monitors; no caller holds a private monitor while trying to reacquire the outer one.private val empty: Semaphore = new FifoBinarySemaphore(true)
private val full: Semaphore = new FifoBinarySemaphore(false)This is a complete implementation when inserted in
SemaphoreMemory: its write and read methods remain exactly the displayed methods. Pending writers obtain empty-slot permissions in registration order on empty; pending readers obtain full-slot permissions in registration order on full. Each handoff permits just one caller to access the slot, soFirst-come-first-served means the order in which concurrent calls register under the monitor; two overlapping calls have no externally determined order. The guarantee is separate for the two kinds of caller. A global order combining reads and writes cannot be required: an initial read must wait while a later write supplies its value. Subject to eventual scheduling and the arrival of matching operations of the opposite kind, no registered caller is overtaken by a later caller of its own kind. The design avoids broadcast wakeups as well as barging.Use one monitor to protect both the occupied flag and the slot.
Option supplies an empty marker without confusing an empty slot with a valid null payload.class MonitorMemory[T] extends Memory[T] {
private var slot: Option[T] = None
def write(msg: T): Unit = synchronized {
while (slot.nonEmpty) wait()
slot = Some(msg)
notifyAll()
}
def read: T = synchronized {
while (slot.isEmpty) wait()
val result = slot.get
slot = None
notifyAll()
result
}
}A writer changes empty to full while holding the monitor; a reader changes full to empty while holding the same monitor. The corresponding successful assignment or removal is its linearization point. No second writer can overwrite a full slot, and no two readers can remove one value. Rechecking in a
while handles spurious wakeups and competition after notification.There are two different waiting predicates on the same monitor: writers need empty, readers need full. A single arbitrary
notify could wake a caller of the wrong kind. For example, after a reader empties the slot, waking another reader leaves it waiting again while a writer remains asleep, potentially stranding the computation. notifyAll prevents that lost wake-up pattern by allowing every waiter to recheck. Thus,With many users, each change wakes numerous software threads even though at most one can use that new slot state. Most reacquire the monitor, find their predicate false and sleep again. This thundering herd wastes context switches and lock acquisitions. It also supplies no fairness or arrival-order guarantee.