Proof of Concept for final project due this Friday.
enq
and deq
methodsAtomicReference
for head
, tail
, next
compareAndSet
to validate & update atomicallyhead
/tail
proactively
deq
/enq
public T deq() throws EmptyException {
while (true) {
Node first = head.get();
Node last = tail.get();
Node next = first.next.get();
if (first == head.get()) {
if (first == last) {
if (next == null) {
throw new EmptyException();
}
tail.compareAndSet(last, next);
} else {
T value = next.value;
if (head.compareAndSet(first, next))
return value;
}
}
}
}
How can we implement a bounded queue (with locks)?
Keep track of size!
UnboundedQueue
enq
and deq
methodsfinal int capacity
fieldAtomicInteger size
field
enq
deq
size <= capacity
Why should size
be atomic?
enqLock
size
is less than capacity
size < capacity
? (partial method)deqLock
size
is greater than 0
size > 0
? (partial method)Suppose we don’t want to throw exceptions
Question: How might we implement this behavior?
public void enq (T value) {
enqLock.lock();
try {
Node nd = new Node(value);
while (size.get() == capacity) { }; // wait until not full
tail.next = nd;
tail = nd;
} finally {
enqLock.unlock();
}
}
This is wasteful!
while (size.get() == capacity) { }; // wait until not full
The thread:
size.get() == capacity
What if it takes a while until the queue is not full?
The following would be better:
size.get() == capacity
size.get() < capacity
Lock
and Condition
InterfacesThe Lock
interface defines a curious method:
Condition newCondition()
returns a new Condition instance that is bound to this Lock instanceThe Condition
interface
void await()
causes the current thread to wait until it is signalled or interruptedvoid signal()
wakes up one waiting threadvoid signalAll()
wakes up all waiting threadsDefine condition for enqLock
notFullCondition
When enqueuing to a full queue
volatile boolean
) for thisnotFullCondition.await()
notFullCondition
is satisfiedWhen dequeueing
notFullCondition.signalAll()
(Similar: notEmptyCondition
for deq
method)
BoundedQueue
public class BoundedQueue<T> implements SimpleQueue<T> {
ReentrantLock enqLock, deqLock;
Condition notEmptyCondition, notFullCondition;
AtomicInteger size;
volatile Node head, tail;
final int capacity;
public BoundedQueue(int capacity) {
this.capacity = capacity;
this.head = new Node(null);
this.tail = this.head;
this.size = new AtomicInteger(0);
this.enqLock = new ReentrantLock();
this.notFullCondition = this.enqLock.newCondition();
this.deqLock = new ReentrantLock();
this.notEmptyCondition = this.deqLock.newCondition();
}
public void enq(T item) { ... }
public T deq() { ... }
class Node { ... }
}
public void enq(T item) {
boolean mustWakeDequeuers = false;
Node nd = new Node(item);
enqLock.lock();
try {
while (size.get() == capacity) {
try {
// System.out.println("Queue full!");
notFullCondition.await();
} catch (InterruptedException e) {
// do nothing
}
}
tail.next = nd;
tail = nd;
if (size.getAndIncrement() == 0) {
mustWakeDequeuers = true;
}
} finally {
enqLock.unlock();
}
if (mustWakeDequeuers) {
deqLock.lock();
try {
notEmptyCondition.signalAll();
} finally {
deqLock.unlock();
}
}
}
public T deq() {
T item;
boolean mustWakeEnqueuers = false;
deqLock.lock();
try {
while (head.next == null) {
try {
// System.out.println("Queue empty!");
notEmptyCondition.await();
} catch(InterruptedException e) {
//do nothing
}
}
item = head.next.item;
head = head.next;
if (size.getAndDecrement() == capacity) {
mustWakeEnqueuers = true;
}
} finally {
deqLock.unlock();
}
if (mustWakeEnqueuers) {
enqLock.lock();
try {
notFullCondition.signalAll();
} finally {
enqLock.unlock();
}
}
return item;
}
What if we want to make a lock-free bounded queue?
size
and capacity
fields to our LockFreeQueue
?Basic operations
void push(T item)
add a new item to the top of the stackT pop()
remove top item from the stack and return it
EmptyException
if stack was emptypush()
Step 1: Create Nodepush()
Step 2: Set next
push()
Step 3: Set head
push()
Completepop()
?pop()
Step 1: Store value
pop()
Step 2: Update head
pop()
Step 3: Return value
With locks:
head
, coarse locking is natural choiceWithout locks?
Use linked-list implementation
top
as an AtomicReference<Node>
compareAndSet
to modify top
top
points to item’s Node
public class LockFreeStack<T> implements SimpleStack<T> {
AtomicReference<Node> top = new AtomicReference<Node>(null);
public void push(T item) {...}
public T pop() throws EmptyException {...}
class Node {
public T value;
public AtomicReference<Node> next;
public Node(T value) {
this.value = value;
this.next = new AtomicReference<Node>(null);
}
}
}
push
public void push(T item) {
Node nd = new Node(item);
Node oldTop = top.get();
nd.next.set(oldTop);
while (!top.compareAndSet(oldTop, nd)) {
oldTop = top.get();
nd.next.set(oldTop);
}
}
pop
public T pop() throws EmptyException {
while (true) {
Node oldTop = top.get();
if (oldTop == null) {
throw new EmptyException();
}
Node newTop = oldTop.next.get();
if (top.compareAndSet(oldTop, newTop)) {
return oldTop.value;
}
}
}
Modifying top
push
/pop
rate limited by top.compareAndSet(...)
… or is it?
Consider several concurrent accesses to a stack:
stk.push(item1)
stk.push(item2)
stk.pop()
stk.push(item4)
stk.pop()
stk.pop()
Trick Question. What is the state of stk
after these calls?
Match and exchange!
Cut out the middleperson!
push
/pop
to stack
push
, try to find a pop
and give them your valuepop
, try to find a push
and take their valueNext time: implement an exchange object to facilitate this