Monitors and Blocking Synchronization
Table of contents Monitors Conditions Readers-Writers Lock Definition Motivation Conditions Queue example Readers-Writers Lock Implementations: Simple and Fair Reentrant Lock Implementation Semaphores Summary
Monitors Monitors – combining synchronization and data An instance of a class that provides mutual exclusion to its methods The monitor is comprised of methods, locks and conditions (more on that later) Invented by C.A.R Hoare and Per Brinch-Hansen in 1974
Monitors (cont.) Motivation: Suppose we have two threads – a producer and a consumer Suppose they share a FIFO queue and a lock to allow access to the queue The code for the producer could look like this: mutex.lock(); try { queue.enq(x); } finally { mutex.unlock();
Monitors (cont.) Problems: When the queue is full the enq method cannot execute and has to wait When there are multiple threads, each must keep track of the data and lock objects and maintain the same locking mechanism in order for the application to be correct The thread doesn’t have access to the internal state of the queue
Monitors (cont.) Solution: Providing the queue class with its own internal lock – each method first acquires it and when it finishes it releases it The enq (deq) method will decide whether to block the producer (consumer)
Monitors (cont.) A monitor is basically a class representing a shared resource comprised of: Locks Conditions Methods that acquire the lock upon calling them and release the lock upon returning from them Thus allowing mutual exclusion
Monitors (cont.) Advantages of using monitors: Synchronization code is written internally in the class - the users don’t need to know about it It works for multiple threads accessing the same shared object With the use of conditions a thread may relinquish the lock if some property does not hold
Monitors (cont.) When a thread fails to acquire a lock it can act in one of two ways: Spin / Busy-wait: Continuously checking whether the lock is available Preferred when short delays are expected Waste of CPU time Block: Withdrawing from trying to acquire the lock and letting other threads try Preferred when long delays are expected Overhead of call to OS to try other threads Possible combination: the thread spins for a short time and blocks after a certain time period
Motivating Condition Variables Another means of allowing concurrent access is the condition variable; before we get into that though, lets look at a situation where we’d need one: Imagine we have several producer threads and several consumer threads Producers do work, toss their results into a buffer Consumers take results off of buffer as they come and process them Ex: Multi-step computation f e d c buffer back front producer(s) enqueue consumer(s) dequeue
Motivating Condition Variables Cooking analogy: Team one peels potatoes, team two takes those and slices them up When a member of team one finishes peeling, they toss the potato into a tub Members of team two pull potatoes out of the tub and dice them up f e d c buffer back front producer(s) enqueue consumer(s) dequeue
Motivating Condition Variables: f e d c buffer back front producer(s) enqueue consumer(s) dequeue If the buffer is empty, consumers have to wait for producers to produce more data If buffer gets full, producers have to wait for consumers to consume some data and clear space We’ll need to synchronize access; why? Data race; simultaneous read/write or write/write to back/front
Conditions Motivation: A Condition is a class associated with a lock When a thread acquiring a lock of an object is waiting for something to happen before he can proceed – it is a good idea for it to release the lock to let other threads access the object A Condition is a class associated with a lock Created using the newCondition() method of the Lock interface When a condition occurs, a thread that is waiting may be awakened to try to access the monitor Part of a well defined monitor
Conditions - methods void await(): The method called when a thread wants to wait for a certain property to hold More variations of await() also exist void signal(): The method called when a thread wants to wake one other thread waiting for this condition (i.e., in await()) Before returning from await() the thread must re-acquire the lock associated with this condition void signalAll(): Same as signal, only all waiting threads are awakened
Conditions (cont.) When signal(All) is called, thread(s) waiting on await() may return one at a time This does not necessarily mean that the property now holds Maybe too many threads were awakened and the property does not hold for all of them Maybe await() returned spuriously The thread should check for the property and then call await() again if it does not hold
Conditions (cont.) The approach we saw that is implemented in Java is called non-blocking condition variables or Signal and Continue Gives priority to the signaling thread and only after it releases the lock the awakened thread can return The original monitor proposed by Hoare and Hansen used blocking condition variables or Signal and Wait The priority of acquiring the lock is given directly to the signaled thread
Monitors in Java Java provides support for monitors by using synchronized methods Also uses built-in methods wait(), notify() and notifyAll() Similar to await(), signal() and signalAll() - only associated with one (implicit) condition variable
class Buffer<E> { E[] array = (E[])new Object[SIZE]; … // front, back fields, isEmpty, isFull methods synchronized void enqueue(E elt) { if(isFull()) ??? else … add to array and adjust back … } synchronized E dequeue() { if(isEmpty()) { … take from array and adjust front … First attempt One approach; if buffer is full on enqueue, or empty on dequeue, throw an exception Not what we want here; w/ multiple threads taking & giving, these will be common occurrences – should not handle like errors Common, and only temporary; will only be empty/full briefly Instead, we want threads to be pause until it can proceed
Pausing void enqueue(E elt) { while(true) { synchronized(this) { enqueue to a full buffer should not raise an exception Wait until there is room dequeue from an empty buffer should not raise an exception Wait until there is data One approach to pausing: spin the lock: loop, checking until buffer is no longer full (for enqueue case) Hold the lock for the check, then release and loop Spinning works… but is very wasteful: We’re using a processor just for looping & checking We’re holding the lock a good deal of the time for that checking Cooking analogy: When waiting for work, team two members reach into tub every few seconds to see if another potato is in there void enqueue(E elt) { while(true) { synchronized(this) { if(isFull()) continue; … add to array and adjust back … return; }}} // dequeue similar
What we want Better would be for a thread to wait until it can proceed Be notified when it should try again Thread suspended until then; in meantime, other threads run While waiting, lock is released; will be re-acquired later by one notified thread Upon being notified, thread just drops in to see what condition it’s condition is in Team two members work on something else until they’re told more potatoes are ready Less contention for lock, and time waiting spent more efficiently
Condition Variables Like locks & threads, not something you can implement on your own Language or library gives it to you An ADT that supports this: condition variable Informs waiting thread(s) when the condition that causes it/them to wait has varied Terminology not completely standard; will mostly stick with Java
Java approach: right idea; some problems in the details class Buffer<E> { … synchronized void enqueue(E elt) { if(isFull()) this.wait(); // releases lock and waits add to array and adjust back if(buffer was empty) this.notify(); // wake somebody up } synchronized E dequeue() { if(isEmpty()) { take from array and adjust front if(buffer was full)
Key ideas Condition variables: A Thread can wait, suspending operation and relinquishing the lock, until it is notified wait: “Register” running thread as interested in being woken up Then atomically: release the lock and block When execution resumes after notify, thread again holds the lock notify: Pick one waiting thread and wake them up No guarantee woken up thread runs next, just that it is no longer blocked on the condition – now waits for the lock If no thread is waiting, then do nothing Java weirdness: every object “is” a condition variable (and a lock) Just like how we can synchronize on any object Other languages/libraries often make them separate
Bug #1 synchronized void enqueue(E elt){ if(isFull()) this.wait(); add to array and adjust back … } Between the time a thread is notified and when it re-acquires the lock, the condition can become false again! if(isFull()) this.wait(); add to array Time Thread 2 (dequeue) Thread 1 (enqueue) take from array if(was full) this.notify(); enqueue; full again Thread 3 (enqueue)
Bug fix #1 synchronized void enqueue(E elt) { while(isFull()) this.wait(); … } synchronized E dequeue() { while(isEmpty()) { Guideline: Always re-check the condition after re-gaining the lock If condition still not met, go back to waiting In fact, for obscure reasons, Java is technically allowed to notify a thread for no reason
Bug #2 If multiple threads are waiting, currently we only wake up one Works for the most part, but what if 2 are waiting to enqueue, and two quick dequeues occur before either gets to go? We’d only notify once; other thread would wait forever if(isFull()) this.wait(); … Time Thread 2 (enqueue) Thread 1 (enqueue) // dequeue #1 if(buffer was full) this.notify(); // dequeue #2 Thread 3 (dequeues)
Bug fix #2 synchronized void enqueue(E elt) { … if(buffer was empty) this.notifyAll(); // wake everybody up } synchronized E dequeue() { if(buffer was full) notifyAll wakes up all current waiters on the condition variable Guideline: If in any doubt, use notifyAll Wasteful waking is better than never waking up So why does notify exist? Well, it is faster when correct…
Alternate approach An alternative is to call notify (not notifyAll) on every enqueue / dequeue, not just when the buffer was empty / full Easy to implement: just remove the if statement Alas, makes our code subtly wrong since it’s technically possible that an enqueue and a dequeue are both waiting Idea: Under extreme cases, the fact that producers and consumers share a condition variable can result in each waiting for the other Details for the curious (not on the final): Buffer is full and so a huge # of enqueues (>SIZE) have to wait So each dequeue wakes up one enqueue, but say so many dequeue calls happen so fast that the buffer is empty and a dequeue call waits The final notify may wake up a dequeue, which immediately has to wait again, and now everybody will wait forever We can fix it; it just involves using a different condition variable for producers and consumers – they still share the same lock though
Last condition-variable comments notify/notifyAll often called signal/broadcast Condition variables are subtle and harder to use than locks Not as common as locks But when you need them, you need them Spinning and other work-arounds don’t work well The common use-cases are already provided efficiently in libraries Example: java.util.concurrent.ArrayBlockingQueue<E> All uses of condition variables hidden in the library; client just calls put and take
enq() and deq() work at different ends of the object Queue: Concurrency enq(x) y=deq() tail head Making a queue concurrent is quite a challenge. Very informally, it seems that it should be OK for one thread to enqueue an item at one end of the queue while another thread dequeues an item from the other end, since they are working on disjoint parts of the data structure. Does that kind of concurrency seem easy to realize? (Answer: next slide) enq() and deq() work at different ends of the object Art of Multiprocessor Programming© Herlihy-Shavit 2007
Challenge: what if the queue is empty or full? Concurrency enq(x) y=deq() head tail Concurrent enqueue and dequeue calls should in principle be able to proceed without interference, but it gets tricky when the queue is nearly full or empty, because then one method call should affect the other. The problem is that whether the two method calls interfere (that is, need to synchronize) depends on the dynamic state of the queue. In pretty much all of the other kinds of synchronization we considered, whether or not method calls needed to synchronize was determined statically (say read/write locks) or was unlikely to change very often (resizable hash tables). Challenge: what if the queue is empty or full? Art of Multiprocessor Programming© Herlihy-Shavit 2007
Bounded Queue Sentinel head tail Let’s use a list-based structure, although arrays would also work. We start out with head and tail fields that point to the first and last entries in the list. The first Node in the list is a sentinel Node whose value field is meaningless. The sentinel acts as a placeholder. Sentinel Art of Multiprocessor Programming© Herlihy-Shavit 2007
Bounded Queue First actual item head tail When we add actual items to the queue, we will hang them of the sentinel Node at the head. First actual item Art of Multiprocessor Programming© Herlihy-Shavit 2007
Lock out other deq() calls Bounded Queue head tail deqLock The most straightforward way to allow concurrent enq and deq calls is to use one lock at each end of the queue. To make sure that only one dequeuer is accessing the queue at a time, we introduce a dequeue lock field. Could we get the same effect just by locking the tail Node? (Answer: there’s a race condition between when a thread reads the tail field and when it acquires the lock. Another thread might have enqueued something in that interval. Of course you could check after you acquire the lock that the tail pointer hadn’t changed, but that seems needlessly complicated.) Lock out other deq() calls Art of Multiprocessor Programming© Herlihy-Shavit 2007
Lock out other enq() calls Bounded Queue head tail deqLock In the same way, we introduce an explicit enqLock field to ensure that only one enqueuer can be accessing the queue at a time. Would it work if we replaced the enqLock by a lock on the first sentinel Node? (Answer: Yes, it would because the value of the head field never changes. Nevertheless we use an enqLock field to be symmetric with the deqLock.) enqLock Lock out other enq() calls Art of Multiprocessor Programming© Herlihy-Shavit 2007
Need to tell whether queue is full or empty Not Done Yet head tail deqLock In the same way, we introduce an explicit enqLock field to ensure that only one enqueuer can be accessing the queue at a time. Would it work if we replaced the enqLock by a lock on the first sentinel Node? (Answer: Yes, it would because the value of the head field never changes. Nevertheless we use an enqLock field to be symmetric with the deqLock.) enqLock Need to tell whether queue is full or empty Art of Multiprocessor Programming© Herlihy-Shavit 2007
Permission to enqueue 8 items Not Done Yet head tail deqLock Let’s add another field, which we will think of as keeping track of permissions to enqueue items. We could also think of this field as counting the number of empty slots, but calling it a permission will seem more natural later on when we look at some additional fine-grained tricks. enqLock permits 8 Permission to enqueue 8 items Art of Multiprocessor Programming© Herlihy-Shavit 2007
Not Done Yet 8 Incremented by deq() Decremented by enq() head tail deqLock This field is incremented by the deq method (which creates a space) and decremented by the enq method. enqLock permits 8 Incremented by deq() Decremented by enq() Art of Multiprocessor Programming© Herlihy-Shavit 2007
Enqueuer Lock enqLock 8 head tail deqLock enqLock permits Let’s walk through a very simple implementation. A thread that wants to enqueue an item first acquires the enqLock. At this point, we know there are no other enq calls in progress. Lock enqLock enqLock permits 8 Art of Multiprocessor Programming© Herlihy-Shavit 2007
Enqueuer Read permits 8 OK head tail deqLock enqLock permits Next, the thread reads the number of permits. What do we know about how this field behaves when the thread is holding the enqLock? (Answer: it can only increase. Dequeuers can increment the counter, but all the enqueuers are locked out.) If the enqueuing thread sees a non-zero value for permits, it can proceed. Read permits enqLock permits 8 OK Art of Multiprocessor Programming© Herlihy-Shavit 2007
Enqueuer No need to lock tail 8 head tail deqLock enqLock permits The thread does not need to lock tail Node before appending a new Node. Clearly, there is no conflict with another enqueuer and we will see a clever way to avoid conflict with a dequeuer. No need to lock tail enqLock permits 8 Art of Multiprocessor Programming© Herlihy-Shavit 2007
Enqueuer Enqueue Node 8 head tail deqLock enqLock permits Still holding the enqLock, the thread redirects both the tail Node’s next field and the queue’s tail field to point to the new Node. enqLock Enqueue Node permits 8 Art of Multiprocessor Programming© Herlihy-Shavit 2007
Enqueuer 8 7 getAndDecrement() head tail deqLock enqLock permits Still holding the enqLock, the thread calls getAndDecrement() to reduce the number of permits. Remember that we know the number is positive, although we don’t know exactly what it is now. Why can’t we release the enqLock before the increment? (Answer: because then other enqueuers won’t know when they have run out of permits.) enqLock permits 7 8 getAndDecrement() Art of Multiprocessor Programming© Herlihy-Shavit 2007
Enqueuer Release lock 8 7 head tail deqLock enqLock permits Finally, we can release the enqLock and return. enqLock permits Release lock 8 7 Art of Multiprocessor Programming© Herlihy-Shavit 2007
If queue was empty, notify/signal waiting dequeuers Enqueuer head tail deqLock There is one more issue to consider. If the queue was empty, then one or more dequeuers may be waiting for an item to appear. If the dequeuer is spinning then we don’t need to do anything, but if the dequeuers are suspended in the usual Java way, then we need to notify them. There are many ways to do this, but for simplicity we will not try to be clever, and simply require any enqueuer who puts an item in an empty queue to acquire the deqLock and notify any waiting threads. TALKING POINT; Notice that Java requires you to acquire the lock for an object before you can notify threads waiting to reacquire that lock. Not clear if this design is a good idea. Does it invite deadlock? Discuss. enqLock If queue was empty, notify/signal waiting dequeuers permits 7 Art of Multiprocessor Programming© Herlihy-Shavit 2007
Unsuccesful Enqueuer Read permits head tail deqLock enqLock permits Let’s rewind and consider what happens if the enqueing thread found the queue full. Suppose the threads synchronize by blocking, say using the Java wait() method. In this case, the thread can wait() on the enqLock, provided the first dequeuer to to remove an item notifies the waiting thread. An alternative is for the enqueuer to spin waiting for the number of permits to be non-zero. In general, it is a dangerous practice to spin while holding a lock. It so happens that it works in this case, provided the dequeuer also spins when the queue is empty. Spinning here is cache-friendly, since the thread is spinning on a locally-cached value, and stops spinning as soon as the field value changes. Read permits enqLock permits Uh-oh Art of Multiprocessor Programming© Herlihy-Shavit 2007
Dequeuer Lock deqLock 8 head tail deqLock enqLock permits Now let’s walk through a similar deq() implementation. The code is similar, but not at all identical. As a first step, the dequeuer thread acquires the deqLock. enqLock Lock deqLock permits 8 Art of Multiprocessor Programming© Herlihy-Shavit 2007
Read sentinel’s next field Dequeuer head tail deqLock Next, the thread reads the sentinel node’s next field. If that field is non-null, the queue is non-empty. Once the enqueuer sees a non-empty next field, that field will remain that way. Why? (ANSWER: the deq() code, which we have seen, changes null references to non-null, but never changes a non-null reference). The enqueuers are all locked out, and no dequeuer will modify a non-null field. There is no need to lock the Node itself, because it is implicitly protected by the enqLock. Read sentinel’s next field enqLock permits 7 OK Art of Multiprocessor Programming© Herlihy-Shavit 2007
Dequeuer Read value 7 head tail deqLock enqLock permits Next, the thread reads the first Node’s value, and stores it in a local variable. Read value enqLock permits 7 Art of Multiprocessor Programming© Herlihy-Shavit 2007
Make first Node new sentinel Dequeuer Make first Node new sentinel head tail deqLock The thread then makes the first Node the new sentinel, and discards the old sentinel. This step may seem obvious, but, in fact, it is enormously clever. If we had tried to physically remove the Node, we would soon become bogged down in quagmire of complications. Instead, we discard the prior sentinel, and transform the prior first real Node into a sentinel. Brilliant. enqLock permits 7 Art of Multiprocessor Programming© Herlihy-Shavit 2007
Dequeuer Increment permits 8 head tail deqLock enqLock permits Finally, we increment the number of permits. By contrast with enqueuers, we do not need to hold the lock while we decrement the permits. Why? (Answer: we had to hold the lock while enqueuing to prevent lots of enqueuers from proceeding without noticing that the capacity had been exceeded. Dequeuers will notice the queue is empty when they observe that the sentinel’s next field is null.) enqLock Increment permits permits 8 Art of Multiprocessor Programming© Herlihy-Shavit 2007
Dequeuer 7 Release deqLock head tail deqLock enqLock permits Next we can release the deqLo ck. enqLock permits 7 Release deqLock Art of Multiprocessor Programming© Herlihy-Shavit 2007
Read sentinel’s next field Unsuccesful Dequeuer head tail deqLock If the dequeuer observes that the sentinel’s next field is null, then it must wait for something to be enqueued. As with the dequeuer, the simplest approach is just to wait() on the deqLock, with the understanding that any enqueuer who makes the queue non-empty will notify any waiting threads. Spinning works too, even though the thread is holding a lock. Clearly, mixing the two strategies will not work: if a dequeuer spins while holding the deq lock and an enqueuer tries to acquire the deq lock to notify waiting threads, then a deadlock ensues. Read sentinel’s next field enqLock permits 8 uh-oh Art of Multiprocessor Programming© Herlihy-Shavit 2007
Bounded Queue public class BoundedQueue<T> { ReentrantLock enqLock, deqLock; Condition notEmptyCondition, notFullCondition; AtomicInteger permits; Node head; Node tail; int capacity; enqLock = new ReentrantLock(); notFullCondition = enqLock.newCondition(); deqLock = new ReentrantLock(); notEmptyCondition = deqLock.newCondition(); } Art of Multiprocessor Programming© Herlihy-Shavit 2007
Bounded Queue Enq & deq locks public class BoundedQueue<T> { ReentrantLock enqLock, deqLock; Condition notEmptyCondition, notFullCondition; AtomicInteger permits; Node head; Node tail; int capacity; enqLock = new ReentrantLock(); notFullCondition = enqLock.newCondition(); deqLock = new ReentrantLock(); notEmptyCondition = deqLock.newCondition(); } Enq & deq locks Art of Multiprocessor Programming© Herlihy-Shavit 2007
The Java Lock Interface public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit); Condition newCondition(); void unlock; } Acquire lock Art of Multiprocessor Programming© Herlihy-Shavit 2007
The Java Lock Interface public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit); Condition newCondition(); void unlock; } Release lock Art of Multiprocessor Programming© Herlihy-Shavit 2007
The Java Lock Interface public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit); Condition newCondition(); void unlock; } Try for lock, but not too hard Art of Multiprocessor Programming© Herlihy-Shavit 2007
The Java Lock Interface public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit); Condition newCondition(); void unlock; } Create condition to wait on Art of Multiprocessor Programming© Herlihy-Shavit 2007
The Java Lock Interface public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit); Condition newCondition(); void unlock; } Java allows threads to interrupt one another. Interrupts are not pre-emptive: a thread typically must check whether it has been interrupted. Some blocking methods, like the built-in wait() method throw InterruptedException if the thread is interrupted while waiting. The lock method of the lock interface does not detect interrupts, presumably because it is more efficient not to do so. This variant does. Guess what this method does? Art of Multiprocessor Programming© Herlihy-Shavit 2007
Lock Conditions public interface Condition { void await(); boolean await(long time, TimeUnit unit); … void signal(); void signalAll(); } Art of Multiprocessor Programming© Herlihy-Shavit 2007
Lock Conditions Release lock and wait on condition public interface Condition { void await(); boolean await(long time, TimeUnit unit); … void signal(); void signalAll(); } Release lock and wait on condition Art of Multiprocessor Programming© Herlihy-Shavit 2007
Wake up one waiting thread Lock Conditions public interface Condition { void await(); boolean await(long time, TimeUnit unit); … void signal(); void signalAll(); } Wake up one waiting thread Art of Multiprocessor Programming© Herlihy-Shavit 2007
Wake up all waiting threads Lock Conditions public interface Condition { void await(); boolean await(long time, TimeUnit unit); … void signal(); void signalAll(); } Wake up all waiting threads Art of Multiprocessor Programming© Herlihy-Shavit 2007
Await Releases lock associated with q Sleeps (gives up processor) q.await() In Java, every object provides a wait() method that unlocks the object and suspends the caller for a while. While the caller is waiting, another thread can lock and change the object. Later, when the suspended thread resumes, it locks the object again before it returns from the wait() call. Releases lock associated with q Sleeps (gives up processor) Awakens (resumes running) Reacquires lock & returns Art of Multiprocessor Programming© Herlihy-Shavit 2007
Signal Awakens one waiting thread Which will reacquire lock q.signal(); The notify() method (similarly notify() for synchronized methods) wakes up one waiting thread, chosen arbitrarily from the set of waiting threads. When that thread awakens, it competes for the lock like any other thread. When that thread reacquires the lock, it returns from its wait() call. You cannot control which waiting thread is chosen. Awakens one waiting thread Which will reacquire lock Art of Multiprocessor Programming© Herlihy-Shavit 2007
Signal All Awakens all waiting threads Which will each reacquire lock q.signalAll(); The signalAll() method wakes up all waiting threads. Each time the object is unlocked, one of these newly- wakened threads will reacquire the lock and return from its wait() call. You cannot control the order in which the threads reacquire the lock. Awakens all waiting threads Which will each reacquire lock Art of Multiprocessor Programming© Herlihy-Shavit 2007
A Monitor Lock waiting room Critical Section Lock() unLock() Art of Multiprocessor Programming© Herlihy-Shavit 2007
Unsuccessful Deq waiting room Critical Section Lock() Deq() await() Oh no, Empty! Art of Multiprocessor Programming© Herlihy-Shavit 2007
Another One waiting room Critical Section Lock() Deq() await() Oh no, Empty! Art of Multiprocessor Programming© Herlihy-Shavit 2007
Enqueur to the Rescue waiting room Critical Section Yawn! Yawn! Lock() signalAll() Critical Section unLock() Art of Multiprocessor Programming© Herlihy-Shavit 2007
Monitor Signalling waiting room Critical Section Awakend thread Yawn! Yawn! waiting room Notice, there may be competition from other threads attempting to lock for the thread. The FIFO order is arbitrary, different monitor locks have different ordering of waiting threads, that is, notify could release the earliest or latest any other waiting thread. Critical Section Awakend thread might still lose lock to outside contender… Art of Multiprocessor Programming© Herlihy-Shavit 2007
Dequeurs Signalled waiting room Critical Section Yawn! Found it Notice, there may be competition from other threads attempting to lock for the thread. The FIFO order is arbitrary, different monitor locks have different ordering of waiting threads, that is, notify could release the earliest or latest any other waiting thread. Found it Critical Section Art of Multiprocessor Programming© Herlihy-Shavit 2007
Dequeurs Signalled waiting room Critical Section Yawn! Still empty! Notice, there may be competition from other threads attempting to lock for the thread. The FIFO order is arbitrary, different monitor locks have different ordering of waiting threads, that is, notify could release the earliest or latest any other waiting thread. Critical Section Still empty! Art of Multiprocessor Programming© Herlihy-Shavit 2007
Dollar Short + Day Late waiting room Critical Section Notice, there may be competition from other threads attempting to lock for the thread. The FIFO order is arbitrary, different monitor locks have different ordering of waiting threads, that is, notify could release the earliest or latest any other waiting thread. Critical Section Art of Multiprocessor Programming© Herlihy-Shavit 2007
Lost Wake-Up waiting room Critical Section Yawn! Lock() Enq( ) Just as locks are inherently vulnerable to deadlock, \cCondition{} objects are inherently vulnerable to \emph{lost wakeups}, in which one or more threads wait forever without realizing that the condition for which they are waiting has become true. Lost wakeups can occur in subtle ways. Figure \ref{figure:monitor:lost} shows an ill-considered optimization of the \cQueue{T} class. Instead of signaling the \fNotEmpty{} condition each time \mEnq{} enqueues an item, would it not be more efficient to signal the condition only when the queue actually transitions from empty to non-empty? This optimization works as intended if there is only one producer and one consumer, but it is incorrect if there are multiple producers or consumers. Consider the following scenario: consumers $A$ and $B$ both try to dequeue an item from an empty queue, both detect the queue is empty, and both block on the \fNotEmpty{} condition. Producer $C$ enqueues an item in the buffer, and signals \fNotEmpty{}, waking $A$. Before $A$ can acquire the lock, however, another producer $D$ puts a second item in the queue, and because the queue is not empty, it does not signal \fNotEmpty{}. Then $A$ acquires the lock, removes the first item, but $B$, victim of a lost wakeup, waits forever even though there is an item in the buffer to be consumed. Although there is no substitute for reasoning carefully about your program, there are simple programming practices that will minimize vulnerability to lost wakeups. signal () Critical Section unLock() Art of Multiprocessor Programming© Herlihy-Shavit 2007
Lost Wake-Up waiting room Critical Section Yawn! Lock() Enq( ) unLock() Art of Multiprocessor Programming© Herlihy-Shavit 2007
Lost Wake-Up waiting room Critical Section Yawn! Art of Multiprocessor Programming© Herlihy-Shavit 2007
Lost Wake-Up waiting room Critical Section Found it . Found it Critical Section Art of Multiprocessor Programming© Herlihy-Shavit 2007
What’s Wrong Here? waiting room Critical Section zzzz….! Art of Multiprocessor Programming© Herlihy-Shavit 2007
Solution to Lost Wakeup Always use signalAll and notifyAll Not signal and notify Art of Multiprocessor Programming© Herlihy-Shavit 2007
Java Synchronized Methods public class Queue<T> { int head = 0, tail = 0; T[QSIZE] items; public synchronized T deq() { while (tail – head == 0) this.wait(); T result = items[head % QSIZE]; head++; this.notifyAll(); return result; } … }} Synchronized methods use a monitor lock also. Art of Multiprocessor Programming© Herlihy-Shavit 2007
Java Synchronized Methods public class Queue<T> { int head = 0, tail = 0; T[QSIZE] items; public synchronized T deq() { while (tail – head == 0) this.wait(); T result = items[head % QSIZE]; head++; this.notifyAll(); return result; } … }} Synchronized methods use a monitor lock also. Each object has an implicit lock with an implicit condition Art of Multiprocessor Programming© Herlihy-Shavit 2007
Java Synchronized Methods public class Queue<T> { int head = 0, tail = 0; T[QSIZE] items; public synchronized T deq() { while (tail – head == 0) this.wait(); T result = items[head % QSIZE]; head++; this.notifyAll(); return result; } … }} Lock on entry, unlock on return Synchronized methods use a monitor lock also. Art of Multiprocessor Programming© Herlihy-Shavit 2007
Java Synchronized Methods public class Queue<T> { int head = 0, tail = 0; T[QSIZE] items; public synchronized T deq() { while (tail – head == 0) this.wait(); T result = items[head % QSIZE]; head++; this.notifyAll(); return result; } … }} Wait on implicit condition Synchronized methods use a monitor lock also. Art of Multiprocessor Programming© Herlihy-Shavit 2007
Java Synchronized Methods public class Queue<T> { int head = 0, tail = 0; T[QSIZE] items; public synchronized T deq() { while (tail – head == 0) this.wait(); T result = items[head % QSIZE]; head++; this.notifyAll(); return result; } … }} Signal all threads waiting on condition Synchronized methods use a monitor lock also. Art of Multiprocessor Programming© Herlihy-Shavit 2007
(Pop!) The Bounded Queue public class BoundedQueue<T> { ReentrantLock enqLock, deqLock; Condition notEmptyCondition, notFullCondition; AtomicInteger permits; Node head; Node tail; int capacity; enqLock = new ReentrantLock(); notFullCondition = enqLock.newCondition(); deqLock = new ReentrantLock(); notEmptyCondition = deqLock.newCondition(); } Art of Multiprocessor Programming© Herlihy-Shavit 2007
Bounded Queue Fields Enq & deq locks public class BoundedQueue<T> { ReentrantLock enqLock, deqLock; Condition notEmptyCondition, notFullCondition; AtomicInteger permits; Node head; Node tail; int capacity; enqLock = new ReentrantLock(); notFullCondition = enqLock.newCondition(); deqLock = new ReentrantLock(); notEmptyCondition = deqLock.newCondition(); } Enq & deq locks Art of Multiprocessor Programming© Herlihy-Shavit 2007
Enq lock’s associated condition Bounded Queue Fields public class BoundedQueue<T> { ReentrantLock enqLock, deqLock; Condition notEmptyCondition, notFullCondition; AtomicInteger permits; Node head; Node tail; int capacity; enqLock = new ReentrantLock(); notFullCondition = enqLock.newCondition(); deqLock = new ReentrantLock(); notEmptyCondition = deqLock.newCondition(); } Enq lock’s associated condition Art of Multiprocessor Programming© Herlihy-Shavit 2007
Num permits: 0 to capacity Bounded Queue Fields public class BoundedQueue<T> { ReentrantLock enqLock, deqLock; Condition notEmptyCondition, notFullCondition; AtomicInteger permits; Node head; Node tail; int capacity; enqLock = new ReentrantLock(); notFullCondition = enqLock.newCondition(); deqLock = new ReentrantLock(); notEmptyCondition = deqLock.newCondition(); } Num permits: 0 to capacity Art of Multiprocessor Programming© Herlihy-Shavit 2007
Bounded Queue Fields Head and Tail public class BoundedQueue<T> { ReentrantLock enqLock, deqLock; Condition notEmptyCondition, notFullCondition; AtomicInteger permits; Node head; Node tail; int capacity; enqLock = new ReentrantLock(); notFullCondition = enqLock.newCondition(); deqLock = new ReentrantLock(); notEmptyCondition = deqLock.newCondition(); } Head and Tail Art of Multiprocessor Programming© Herlihy-Shavit 2007
Enq Method Part One public void enq(T x) { boolean mustWakeDequeuers = false; enqLock.lock(); try { while (permits.get() == 0) notFullCondition.await(); Node e = new Node(x); tail.next = e; tail = e; if (permits.getAndDecrement() == capacity) mustWakeDequeuers = true; } finally { enqLock.unlock(); } … A remarkable aspect of this queue implementation is that the methods are subtle, but they fit on a single slide. The \mEnq{} method (Figure~\ref{figure:boundedQueue:enq}) works as follows. A thread acquires the \fEnqLock{} (Line \ref{line:bounded:lock}), and repeatedly reads the \fPermits{} field (Line \ref{line:bounded:permits}). While that field is zero, the queue is full, and the enqueuer must wait until a dequeuer makes room. The enqueuer waits by waiting on the \fNotFullCondition{} field (Line \ref{line:bounded:notfull}), which releases the enqueue lock temporarily, and blocks until the condition is signaled. Each time the thread awakens it checks whether the \fPermits{} field is positive (that is, the queue is not empty) and if not, goes back to sleep. Art of Multiprocessor Programming© Herlihy-Shavit 2007
Lock and unlock enq lock Enq Method Part One public void enq(T x) { boolean mustWakeDequeuers = false; enqLock.lock(); try { while (permits.get() == 0) notFullCondition.await(); Node e = new Node(x); tail.next = e; tail = e; if (permits.getAndDecrement() == capacity) mustWakeDequeuers = true; } finally { enqLock.unlock(); } … Lock and unlock enq lock A remarkable aspect of this queue implementation is that the methods are subtle, but they fit on a single slide. The \mEnq{} method (Figure~\ref{figure:boundedQueue:enq}) works as follows. A thread acquires the \fEnqLock{} (Line \ref{line:bounded:lock}), and repeatedly reads the \fPermits{} field (Line \ref{line:bounded:permits}). While that field is zero, the queue is full, and the enqueuer must wait until a dequeuer makes room. The enqueuer waits by waiting on the \fNotFullCondition{} field (Line \ref{line:bounded:notfull}), which releases the enqueue lock temporarily, and blocks until the condition is signaled. Each time the thread awakens it checks whether the \fPermits{} field is positive (that is, the queue is not empty) and if not, goes back to sleep. Art of Multiprocessor Programming© Herlihy-Shavit 2007
If queue is full, patiently await further instructions … Enq Method Part One public void enq(T x) { boolean mustWakeDequeuers = false; enqLock.lock(); try { while (permits.get() == 0) notFullCondition.await(); Node e = new Node(x); tail.next = e; tail = e; if (permits.getAndDecrement() == capacity) mustWakeDequeuers = true; } finally { enqLock.unlock(); } … Once the number of permits exceeds zero, however, the enqueuer may proceed. Note that once the enqueuer observes a positive number of permits, then while the enqueue is in progress no other thread can cause the number of permits to fall back to zero, because all the other enqueuers are locked out, and a concurrent dequeuer can only increase the number of permits. We must check carefully that this implementation does not suffer from a ``lost-wakeup'' bug. Care is needed because an enqueuer encounters a full queue in two steps: first, it sees that \fPermits{} is zero, and second, it waits on the \fNotFullCondition{} condition until there is room in the queue. When a dequeuer changes the queue from full to not-full, it acquires \fEnqLock{} and signals the \fNotFullCondition{} condition. Even though the \fPermits{} field is not protected by the \fEnqLock{}, the dequeuer acquires the \fEnqLock{} before it signals the condition, so the dequeuer cannot signal between the enqueuer's two steps. If queue is full, patiently await further instructions … Art of Multiprocessor Programming© Herlihy-Shavit 2007
How do we know the permits field won’t change? Be Afraid public void enq(T x) { boolean mustWakeDequeuers = false; enqLock.lock(); try { while (permits.get() == 0) notFullCondition.await(); Node e = new Node(x); tail.next = e; tail = e; if (permits.getAndDecrement() == capacity) mustWakeDequeuers = true; } finally { enqLock.unlock(); } … Once the number of permits exceeds zero, however, the enqueuer may proceed. Note that once the enqueuer observes a positive number of permits, then while the enqueue is in progress no other thread can cause the number of permits to fall back to zero, because all the other enqueuers are locked out, and a concurrent dequeuer can only increase the number of permits. We must check carefully that this implementation does not suffer from a ``lost-wakeup'' bug. Care is needed because an enqueuer encounters a full queue in two steps: first, it sees that \fPermits{} is zero, and second, it waits on the \fNotFullCondition{} condition until there is room in the queue. When a dequeuer changes the queue from full to not-full, it acquires \fEnqLock{} and signals the \fNotFullCondition{} condition. Even though the \fPermits{} field is not protected by the \fEnqLock{}, the dequeuer acquires the \fEnqLock{} before it signals the condition, so the dequeuer cannot signal between the enqueuer's two steps. How do we know the permits field won’t change? Art of Multiprocessor Programming© Herlihy-Shavit 2007
Enq Method Part One Add new node public void enq(T x) { boolean mustWakeDequeuers = false; enqLock.lock(); try { while (permits.get() == 0) notFullCondition.await(); Node e = new Node(x); tail.next = e; tail = e; if (permits.getAndDecrement() == capacity) mustWakeDequeuers = true; } finally { enqLock.unlock(); } … Add new node Art of Multiprocessor Programming© Herlihy-Shavit 2007
If queue was empty, wake frustrated dequeuers Enq Method Part One public void enq(T x) { boolean mustWakeDequeuers = false; enqLock.lock(); try { while (permits.get() == 0) notFullCondition.await(); Node e = new Node(x); tail.next = e; tail = e; if (permits.getAndDecrement() == capacity) mustWakeDequeuers = true; } finally { enqLock.unlock(); } … If queue was empty, wake frustrated dequeuers Art of Multiprocessor Programming© Herlihy-Shavit 2007
Enq Method Part Deux public void enq(T x) { … if (mustWakeDequeuers) { deqLock.lock(); try { notEmptyCondition.signalAll(); } finally { deqLock.unlock(); } A remarkable aspect of this queue implementation is that the methods are subtle, but they fit on a single slide. The \mEnq{} method (Figure~\ref{figure:boundedQueue:enq}) works as follows. A thread acquires the \fEnqLock{} (Line \ref{line:bounded:lock}), and repeatedly reads the \fPermits{} field (Line \ref{line:bounded:permits}). While that field is zero, the queue is full, and the enqueuer must wait until a dequeuer makes room. The enqueuer waits by waiting on the \fNotFullCondition{} field (Line \ref{line:bounded:notfull}), which releases the enqueue lock temporarily, and blocks until the condition is signaled. Each time the thread awakens it checks whether the \fPermits{} field is positive (that is, the queue is not empty) and if not, goes back to sleep. Art of Multiprocessor Programming© Herlihy-Shavit 2007
Are there dequeuers to be signaled? Enq Method Part Deux public void enq(T x) { … if (mustWakeDequeuers) { deqLock.lock(); try { notEmptyCondition.signalAll(); } finally { deqLock.unlock(); } A remarkable aspect of this queue implementation is that the methods are subtle, but they fit on a single slide. The \mEnq{} method (Figure~\ref{figure:boundedQueue:enq}) works as follows. A thread acquires the \fEnqLock{} (Line \ref{line:bounded:lock}), and repeatedly reads the \fPermits{} field (Line \ref{line:bounded:permits}). While that field is zero, the queue is full, and the enqueuer must wait until a dequeuer makes room. The enqueuer waits by waiting on the \fNotFullCondition{} field (Line \ref{line:bounded:notfull}), which releases the enqueue lock temporarily, and blocks until the condition is signaled. Each time the thread awakens it checks whether the \fPermits{} field is positive (that is, the queue is not empty) and if not, goes back to sleep. Are there dequeuers to be signaled? Art of Multiprocessor Programming© Herlihy-Shavit 2007
Lock and unlock deq lock Enq Method Part Deux public void enq(T x) { … if (mustWakeDequeuers) { deqLock.lock(); try { notEmptyCondition.signalAll(); } finally { deqLock.unlock(); } Lock and unlock deq lock The deq() method is symmetric. Art of Multiprocessor Programming© Herlihy-Shavit 2007
Signal dequeuers that queue no longer empty Enq Method Part Deux public void enq(T x) { … if (mustWakeDequeuers) { deqLock.lock(); try { notEmptyCondition.signalAll(); } finally { deqLock.unlock(); } Signal dequeuers that queue no longer empty The deq() method is symmetric. Art of Multiprocessor Programming© Herlihy-Shavit 2007
The Enq() & Deq() Methods Share no locks That’s good But do share an atomic counter Accessed on every method call That’s not so good Can we alleviate this bottleneck? The key insight is that the enqueuer only decrements the counter, and really cares only whether or not the counter value is zero. Symmetrically, the dequeuer cares only whether or not the counter value is the queue capacity. NB: the deq() method does not explicitly check the permits field, but relies on testing the sentinel Node’s next field. Same thing. Art of Multiprocessor Programming© Herlihy-Shavit 2007
Split the Counter The enq() method The deq() method Decrements only Cares only if value is zero The deq() method Increments only Cares only if value is capacity The key insight is that the enqueuer only decrements the counter, and really cares only whether or not the counter value is zero. Symmetrically, the dequeuer cares only whether or not the counter value is the queue capacity. NB: the deq() method does not explicitly check the permits field, but relies on testing the sentinel Node’s next field. Same thing. Art of Multiprocessor Programming© Herlihy-Shavit 2007
Split Counter Enqueuer decrements enqSidePermits Dequeuer increments deqSidePermits When enqueuer runs out Locks deqLock Transfers permits Intermittent synchronization Not with each method call Need both locks! (careful …) Let’s summarize. We split the permits field in to two parts. The enqueuer decrements the enqSidePermits field, and the dequeuer increments the deqSidePermits field. When the enqueuer discovers that its field is zero, then it tries to transfer the dequeuer’s permits to itself. Why do this? It replaces synchronization with each method call with sporadic, intermittent synchronization. As a practical matter, an enqueuer that runs out of permits needs to acquire the dequeue lock (why?) (ANSWER: to prevent the dequeuer from incrementing the value at the same time we are trying to copy it). WARNING: Any time you try to acquire a lock while holding another, your ears should prick up (alt: your spider-sense should tingle) because you are just asking for a deadlock. So far, no danger, because all of the methods seen so far never hold more than one lock at a time. Art of Multiprocessor Programming© Herlihy-Shavit 2007
Conditions (cont.) The Lost-Wakeup Problem Scenario: A thread might continue waiting even though the property it is waiting for has become true Scenario: Instead of calling the notEmpty signal whenever there is a dequeue, notEmpty is signaled only when moving from empty state (i.e., one element in queue) Multiple producers and/or consumers if (count == 1) { notEmpty.signal(); }
Conditions (cont.) The Lost-Wakeup Problem scenario (cont.): A and B try to dequeue an empty queue and wait on the notEmpty condition C enqueues and signals notEmpty, waking A D enqueues before A dequeues – not signaling notEmpty A dequeues, B does not B suffers a lost-wakeup
Conditions (cont.) Rules of thumb to avoid Lost-Wakeup: Understand the code Signal all waiting threads Wait with a timeout Though both could wake threads unnecessarily
Readers-Writers Locks Readers – return object data without modifying it Writers – modify object data Two locks – one for readers and one for writers Many shared objects exhibit many reader calls and few writer calls
Readers-Writers Locks (cont.) ReadWriteLock interface: Should follow two safety properties: A thread can acquire the write lock only if there are no other threads holding the read or write lock A thread can acquire the read lock only if there is no thread holding the write lock Thus multiple readers can acquire the read lock simultaneously public interface ReadWriteLock { Lock readLock(); Lock writeLock(); }
Simple Readers-Writers Lock SimpleReadWriteLock Class Contains a boolean variable (writer) indicating whether there is a thread holding the write lock Only when writer == false can a thread acquire the read lock Contains a counter (readers) indicating how many threads hold the read lock Only when writer == false and readers == 0 can a thread acquire the write lock
Simple Readers-Writers Lock (cont.) lock() and unlock() methods are accessed through objects of inner classes, ReadLock and WriteLock Contains one condition for both writes and reads
SimpleReadWriteLock implementation public class SimpleReadWriteLock implements ReadWriteLock { int readers; boolean writer; Lock lock; Lock readLock, writeLock; Condition condition; public SimpleReadWriteLock() { writer = false; readers = 0; lock = new ReentrantLock(); readLock = new ReadLock(); writeLock = new WriteLock(); condition = lock.newCondition(); } … One lock variable to synchronize lock() and unlock() methods of both WriteLock() and ReadLock() More on reentrant locks later
SimpleReadWriteLock implementation (cont.) public Lock readLock() { return readLock; } public Lock writeLock() { return writeLock; ... Returns instance of the ReadLock inner class Returns instance of the WriteLock inner class
ReadLock inner class class ReadLock implements Lock { ... public void lock() { lock.lock(); try { while (writer) { condition.await(); } readers++; } finally { lock.unlock(); … ... public void unlock() { lock.lock(); try { readers--; if (readers == 0) { condition.signalAll(); } } finally { lock.unlock(); Wake all threads Wait for writer to finish
WriteLock inner class private class WriteLock implements Lock { public void lock() { lock.lock(); try { while (readers > 0 || writer) { condition.await(); } writer = true; } finally { lock.unlock(); ... public void unlock() { lock.lock(); try{ writer = false; condition.signalAll(); }finally { lock.unlock(); } Wait for readers or writer to finish
SimpleReadWriteLock (cont.) Read and Write lock methods synchronize on same lock and condition of the SimpleReadWriteLock class Advantages: Correct – allows mutual exclusion Simple Disadvantage: Fairness does not hold – many readers and few writers may cause starvation of writers One writer could wait while readers keep acquiring the read lock
Fair Readers-Writers Lock Ensuring that once a writer has requested the write lock, no more readers can acquire the read lock Current readers will finish and writer will be able to acquire the write lock
Fair Readers-Writers Lock (cont.) A possible implementation: a FIFO Readers-Writers Lock The readers counter field is replaced by two fields – readAcquires (number of readers who acquired the read lock) and readReleases (number of readers who released the read lock)
Fair Readers-Writers Lock (cont.) The writer field acts as a lock: Writers set it to true when requesting the write lock even when there are current readers active Readers who see “writer == true” can not increment the ReadAcquires field Conclusion: when a thread calls for WriteLock.lock() it has priority over subsequent ReadLock.lock() calls Writers are informed that there are no more readers when a reader releasing the read lock identifies that readAcquires == readReleases
FifoReadWriteLock Implementation public class FifoReadWriteLock implements ReadWriteLock { int readAcquires, readReleases; boolean writer; Lock lock; Condition condition; Lock readLock, writeLock; public FifoReadWriteLock() { readAcquires = readReleases = 0; writer = false; lock = new ReentrantLock(); condition = lock.newCondition(); readLock = new ReadLock(); writeLock = new WriteLock(); } public Lock readLock() { return readLock; public Lock writeLock() { return writeLock; ... readAcquires and readReleases fields
ReadLock inner class No more readers holding the read lock private class ReadLock implements Lock { public void lock() { lock.lock(); while (writer) { condition.await(); } readAcquires++; } finally { lock.unlock(); public void unlock() { try { readReleases++; if (readAcquires == readReleases) condition.signalAll(); No more readers holding the read lock
WriteLock inner class private class WriteLock implements Lock { public void lock() { lock.lock(); try { while (writer) condition.await(); writer = true; while (readAcquires != readReleases) } finally { lock.unlock(); } public void unlock() { writer = false; condition.signalAll(); Waiting for current writer to finish The main change – the writer marks it has acquired the write lock even before current readers have finished Waiting for current readers to finish
Reentrant Lock A lock is reentrant if the same thread can acquire it multiple times Useful when a thread locks a lock and then calls for a nested method that locks it again The locks package provides reentrant locks but we’ll show how to create one from a simple (non-reentrant) lock next
Reentrant Lock (cont.) Example A: A writer thread calling for a write lock for the second time will deadlock with itself Example B (using a fair writers-readers lock): *Taken from http://tutorials.jenkov.com/java-concurrency/read-write-locks.html Thread 1 acquires the read lock Thread II tries to acquire the write lock and is blocked Thread I tries to acquire the read lock again and is blocked because of thread II - deadlock
Reentrant Lock (cont.) The ReentrantReadWriteLock class provided by java in the java.util.concurrent.locks package does not allow “lock upgrades” I.e., a thread holding a lock in read mode and trying to access it in write mode Lock upgrades could cause a thread to deadlock with itself – the write lock waits for the read lock to finish
Our Own Reentrant Lock Identifies the ThreadID of the thread who locks the lock Changes when no other thread is holding the lock and a new thread acquires it Counts the number of times the owner thread locked this lock Incremented when the same thread calls lock(), decremented when the thread calls unlock() When this counter is zero one waiting thread is awakened
SimpleReentrantLock implementation public class SimpleReentrantLock implements Lock{ Lock lock; Condition condition; int owner, holdCount; public SimpleReentrantLock() { lock = new SimpleLock(); condition = lock.newCondition(); owner = 0; holdCount = 0; } public void lock() { int me = ThreadID.get(); lock.lock(); if (owner == me) { holdCount++; return; while (holdCount != 0) { condition.await(); owner = me; holdCount = 1; The thread ID of the current owner of the lock Number of times the owner thread has acquired the lock If same thread acquires lock – holdCount is incremented In case holdCount == 0 owner is updated
SimpleReentrantLock implementation (cont.) public void unlock() { lock.lock(); try { if (holdCount == 0 || owner != ThreadID.get()) throw new IllegalMonitorStateException(); holdCount--; if (holdCount == 0) { condition.signal(); } } finally { lock.unlock(); Illegal unlock The lock is free so one waiting thread is awakened
Semaphore So far we’ve seen mutual exclusion locks where only one thread can access a critical section at the same time Semaphore is a lock with a capacity – an upper bound on the number of threads that can hold it simultaneously Uses a counter to count the number of threads currently holding the lock Incremented when a thread acquires a lock, decremented when a thread releases a lock
Semaphore (cont.) Uses two methods: acquire() – also known in the literature as P() – for acquiring one of the semaphore locks release() – also known as V() – for releasing one of the semaphore locks
Semaphore - implementation public class Semaphore { final int capacity; int state; Lock lock; Condition condition; public Semaphore(int c) { capacity = c; state = 0; lock = new ReentrantLock(); condition = lock.newCondition(); } public void acquire() { lock.lock(); try { while (state == capacity) { condition.await(); state++; } finally { lock.unlock(); Counter indicating number of threads in the critical section A thread released space in the semaphore – waiting threads are awakened Capacity of semaphore public void release() { lock.lock(); try { state--; condition.signalAll(); } finally { lock.unlock(); } Semaphore is full – a thread needs to wait
Summary Monitors allow the control of locks and conditions from within a shared object Conditions associated with locks allow threads to wait and release the lock until some property to hold Readers-writers locks manage access to a shared object when there are both reading and modifying threads Various implementations exist Reentrant locks allow for a thread to acquire a lock it is already holding Semaphores allow multiple threads to access the critical section simultaneously