Use AtomicMarkableReference<Node>
for fields
mark
indicates logical removalFor add
/remove
:
remove
)compareAndSet
to atomically
next
field of predecessorFor contains
:
Traverse are remove:
pred
and curr
where item
livesReturn: a Window
class Window {
public Node pred;
public Node curr;
public Window (Node pred, Node curr) {
this.pred = pred;
this.curr = curr;
}
}
public Window find(Node head, long key) {
Node pred = null;
Node curr = null;
Node succ = null;
boolean[] marked = {false};
boolean snip;
retry: while(true) {
pred = head;
curr = pred.next.getReference();
while(true) {
succ = curr.next.get(marked);
while(marked[0]) {
snip = pred.next.compareAndSet(curr, succ, false, false);
if (!snip) continue retry;
curr = pred.next.getReference();
succ = curr.next.get(marked);
}
if (curr.key >= key) {
return new Window(pred, curr);
}
pred = curr;
curr = succ;
}
}
public boolean remove(T item) {
int key = item.hashCode();
boolean snip;
while (true) {
Window window = find(head, key);
Node pred = window.pred;
Node curr = window.curr;
if (curr.key != key) {
return false;
}
Node succ = curr.next.getReference();
snip = curr.next.compareAndSet(succ, succ, false, true);
if (!snip) {
continue;
}
pred.next.compareAndSet(curr, succ, false, false);
return true;
}
}
Runtimes: 1M Operations
n elts | coarse | fine | optimistic | lazy | non-block |
---|---|---|---|---|---|
10 | 0.11 s | 0.14 s | 0.13 s | 0.11 s | 0.11 s |
100 | 0.14 s | 0.46 s | 0.19 s | 0.13 s | 0.20 s |
1000 | 1.1 s | 3.9 s | 2.2 s | 1.1 s | 2.8 s |
10000 | 28 s | 39 s | 59 s | 29 s | 66 s |
Runtimes: 1M Operations
n elts | coarse | fine | optimistic | lazy | non-block |
---|---|---|---|---|---|
10 | 0.21 s | 0.36 s | 0.33 s | 0.33 s | 0.21 s |
100 | 0.27 s | 1.80 s | 0.38 s | 0.12 s | 0.07 s |
1000 | 1.8 s | 4.7 s | 0.86 s | 0.19 s | 0.49 s |
10,000 | 32 s | 17 s | 9.2 s | 4.7 s | 9.2 s |
Note: fewer elements $\implies$ greater contention
Many ways of achieving correct behavior
Tradeoffs:
Recently:
Questions:
Pool<T>
InterfaceWant to model producer/consumer problem
A pool stores items between production and consumption:
public interface Pool<T> {
void put(T item); // add item to the pool
T get(); // remove item from pool
}
get
from empty pool waits for a put
put
to full pool waits for a get
put
call waits for get
Can use a queue to implement a pool:
enq
implements put
deq
implements get
Two queue implementations today
Node head
sentinal
deq
returns head.next
value (if any), updates head
Node tail
enq
updates tail.next
, updates tail
enqLock
locks enq
operationdeqLock
locks deq
operationNode
s are not lockeddeqLock
head
enqLock
tail.next
tail
UnboundedQueue
in Codepublic class UnboundedQueue<T> implements SimpleQueue<T> {
final ReentrantLock enqLock;
final ReentrantLock deqLock;
volatile Node head;
volatile Node tail;
public UnboundedQueue() {
head = new Node(null);
tail = head;
enqLock = new ReentrantLock();
deqLock = new ReentrantLock();
}
public T deq() throws EmptyException {
T value;
deqLock.lock();
try {
if (head.next == null) {
throw new EmptyException();
}
value = head.next.value;
head = head.next;
} finally {
deqLock.unlock();
}
return value;
}
public void enq (T value) {
enqLock.lock();
try {
Node nd = new Node(value);
tail.next = nd;
tail = nd;
} finally {
enqLock.unlock();
}
}
class Node {
final T value;
volatile Node next;
public Node (T value) {
this.value = value;
}
}
}
Easy to reason about:
enq
enqLock
deq
deqLock
What about concurrent calls to enq
/deq
?