Advanced Locking Techniques
•
•
•
•

Coarse-grained Synchronization
Fine-grained Synchronization
Optimistic Synchronization
Lazy Synchronization
List-based Set
head

pred

curr

tail

c

a

b

head

pred
a

tail

curr
b

c

class node {
T item; int key; Node next;
}
Coarse-grained Synchronization
• Take a sequential implementation, add a lock
field, and ensure that each method call
acquires and releases this lock
• Simple, and works well when levels of
concurrency is low
• When too many threads try to access the
object at the same time, the object becomes a
sequential bottleneck
Coarse-grained Locking
public class CoarseList<T> {
private Node head;
private Lock lock = new ReentrantLock ();
public CoarseList () {
head = new Node (Integer.MIN_VALUE);
head.next = new Node (Integer.MAX_VALUE);
}
public boolean add (T item) {
Node pred, curr;
int key = item.hashCode ();
lock.lock ();
try {
pred = head;
curr = pred.next;
while (curr.key < key) {
pred = curr; curr = curr.next;
}
if (key == curr.key) {
return false;
} else {
Node node = new Node (item);
node.next = curr; pred.next = node;
return true;
}
finally {
lock.unlock ();
}
}
}

public boolean remove (T item) {
Node pred, curr;
int key = item.hashCode ();
lock.lock ();
try {
pred = head;
curr = pred.next;
while (curr.key < key) {
pred = curr; curr = curr.next;

}

}
if (key = curr.key) {
pred.next = curr.next;
return true;
} else {
return false;
}
} finally {
lock.unlock ();
}
Fine-grained Synchronization
• Improve concurrency by locking individual
nodes, instead of locking the entire list
• Operations interfere only when trying to
access the same node at the same time
• Higher concurrency, but requires a lock to be
added for each node
Fine-grained Locking
public class CoarseList<T> {
private Node head;
public CoarseList () {
head = new Node (Integer.MIN_VALUE);
head.next = new Node (Integer.MAX_VALUE);
}
public boolean add (T item) {
int key = item.hashCode ();
head.lock ();
Node pred = head;
try {
Node curr = pred.next;
curr.lock ();
try {
while (curr.key < key) {
pred.unlock();
pred = curr; curr = curr.next;
curr.lock ();
}
if (key == curr.key) {
return false;
}
Node newNode = new Node (item);
newNode.next = curr;
pred.next = newNode;
return true;
} finally {
curr.unlock ();
}
} finally {
pred.unlock ();
}
}

}

public boolean remove (T item) {
Node pred = null, curr = null;
int key = item.hashCode ();
head.lock ();
try {
pred = head;
curr = pred.next;
curr.lock();
try {
while (curr.key < key) {
pred.unlock ();
pred = curr; curr = curr.next;
curr.lock ();
}
if (key = curr.key) {
pred.next = curr.next;
return true;
}
return false;
} finally {
curr.unlock ();
}
} finally {
pred.unlock ();
}
}
Why Two Locks?
head

pred
a

Thread A

Thread B

tail

curr
b

c
Hand-Over-Hand Locking
head

pred
a

Thread A

tail

curr
b

c

Thread B

Except for the initial head node, acquire the lock for curr only
while holding the lock for pred
Optimistic Synchronization
• Search a component without acquiring any
locks
• When a node is found, it locks the
component, and then checks whether the
component has been changed
• Optimistic about the possibility of conflicting
Optimistic Locking (1)
public boolean add (T item) {
int key = item.hashCode ();
while (true) {
Node pred = head;
Node curr = pred.next;
while (curr.key <= key) {
pred = curr; curr = curr.next;
}
pred.lock(); curr.lock ();
try {
if (validate(pred, curr)) {
if (curr.key == key) {
return false;
} else {
Node node = new Node
(item);
node.next = curr; pred.next
= node;
return true;
}
}
} finally {
pred.unlock (); curr.unlock();
}
}
}

public boolean remove (T item) {
int key = item.hashCode ();
while (true) {
Node pred = head;
Node curr = pred.next;
while (curr.key < key) {
pred = curr; curr = curr.next;
}
pred.lock(); curr.lock ();
try {
if (validate(pred, curr)) {
if (curr.key == key) {
pred.next = curr.next;
return true;
} else {
return flase;
}
}
} finally {
pred.unlock (); curr.unlock();
}
}
}
Optimistic Locking (2)
public boolean contains (T item) {
int key = item.hashCode ();
while (true) {
Node pred = head;
Node curr = pred.next;
while (curr.key < key) {
pred = curr; curr = curr.next;
}
try {
pred.lock(); curr.lock ();
if (validate(pred, curr)) {
return (curr.key = key);
}
} finally {
pred.unlock (); curr.unlock();
}
}
}

public boolean validate (
Node pred, Node curr) {
Node node = head;
while (node.key <= pred.key) {
if (node == pred)
return pred.next = curr;
node = node.next;
}
return false;
}
Why validation?
head

pred

tail

curr
Lazy Synchronization
• Postpone significant changes, e.g., physically
removing a node from a linked list
• Allow a list to be traversed only once without
locking, and contains() is wait-free.
• Require a new field marked to be added into
each node
Lazy Locking (1)
public boolean add (T item) {
int key = item.hashCode ();
while (true) {
Node pred = head;
Node curr = pred.next;
while (curr.key < key) {
pred = curr; curr = curr.next;
}
pred.lock();
try {
curr.lock();
try {
if (validate(pred, curr)) {
if (curr.key == key) {
return false;
} else {
Node node = new Node
(item);
node.next = curr;
pred.next = node;
return true;
}
}
} finally {
curr.unlock();
}
} finally {
pred.unlock ();
}
}
}

public boolean remove (T item) {
int key = item.hashCode ();
while (true) {
Node pred = head;
Node curr = pred.next;
while (curr.key < key) {
pred = curr; curr = curr.next;
}
pred.lock();
try {
curr.lock ();
try {
if (validate(pred, curr)) {
if (curr.key != key) {
return false;
} else {
curr.marked = true;
pred.next = curr.next;
return flase;
}
}
} finally {
curr.unlock ();
}
} finally {
pred.unlock ();
}
}
}
Lazy Locking (2)
public boolean contains (T item) {
int key = item.hashCode ();
Node curr = head;
while (curr.key < key) {
curr = curr.next;
}
return curr.key == key && !
curr.marked;
}

private boolean validate (Node pred,
Node curr) {
return !pred.marked && !curr.marked
&& pred.next = curr;
}
Can we do better?
• Non-blocking synchronization: eliminate locks
entirely, relying on built-in atomic operations
such as compareAndSet() for synchronization

Advanced locking

  • 1.
    Advanced Locking Techniques • • • • Coarse-grainedSynchronization Fine-grained Synchronization Optimistic Synchronization Lazy Synchronization
  • 2.
  • 3.
    Coarse-grained Synchronization • Takea sequential implementation, add a lock field, and ensure that each method call acquires and releases this lock • Simple, and works well when levels of concurrency is low • When too many threads try to access the object at the same time, the object becomes a sequential bottleneck
  • 4.
    Coarse-grained Locking public classCoarseList<T> { private Node head; private Lock lock = new ReentrantLock (); public CoarseList () { head = new Node (Integer.MIN_VALUE); head.next = new Node (Integer.MAX_VALUE); } public boolean add (T item) { Node pred, curr; int key = item.hashCode (); lock.lock (); try { pred = head; curr = pred.next; while (curr.key < key) { pred = curr; curr = curr.next; } if (key == curr.key) { return false; } else { Node node = new Node (item); node.next = curr; pred.next = node; return true; } finally { lock.unlock (); } } } public boolean remove (T item) { Node pred, curr; int key = item.hashCode (); lock.lock (); try { pred = head; curr = pred.next; while (curr.key < key) { pred = curr; curr = curr.next; } } if (key = curr.key) { pred.next = curr.next; return true; } else { return false; } } finally { lock.unlock (); }
  • 5.
    Fine-grained Synchronization • Improveconcurrency by locking individual nodes, instead of locking the entire list • Operations interfere only when trying to access the same node at the same time • Higher concurrency, but requires a lock to be added for each node
  • 6.
    Fine-grained Locking public classCoarseList<T> { private Node head; public CoarseList () { head = new Node (Integer.MIN_VALUE); head.next = new Node (Integer.MAX_VALUE); } public boolean add (T item) { int key = item.hashCode (); head.lock (); Node pred = head; try { Node curr = pred.next; curr.lock (); try { while (curr.key < key) { pred.unlock(); pred = curr; curr = curr.next; curr.lock (); } if (key == curr.key) { return false; } Node newNode = new Node (item); newNode.next = curr; pred.next = newNode; return true; } finally { curr.unlock (); } } finally { pred.unlock (); } } } public boolean remove (T item) { Node pred = null, curr = null; int key = item.hashCode (); head.lock (); try { pred = head; curr = pred.next; curr.lock(); try { while (curr.key < key) { pred.unlock (); pred = curr; curr = curr.next; curr.lock (); } if (key = curr.key) { pred.next = curr.next; return true; } return false; } finally { curr.unlock (); } } finally { pred.unlock (); } }
  • 7.
    Why Two Locks? head pred a ThreadA Thread B tail curr b c
  • 8.
    Hand-Over-Hand Locking head pred a Thread A tail curr b c ThreadB Except for the initial head node, acquire the lock for curr only while holding the lock for pred
  • 9.
    Optimistic Synchronization • Searcha component without acquiring any locks • When a node is found, it locks the component, and then checks whether the component has been changed • Optimistic about the possibility of conflicting
  • 10.
    Optimistic Locking (1) publicboolean add (T item) { int key = item.hashCode (); while (true) { Node pred = head; Node curr = pred.next; while (curr.key <= key) { pred = curr; curr = curr.next; } pred.lock(); curr.lock (); try { if (validate(pred, curr)) { if (curr.key == key) { return false; } else { Node node = new Node (item); node.next = curr; pred.next = node; return true; } } } finally { pred.unlock (); curr.unlock(); } } } public boolean remove (T item) { int key = item.hashCode (); while (true) { Node pred = head; Node curr = pred.next; while (curr.key < key) { pred = curr; curr = curr.next; } pred.lock(); curr.lock (); try { if (validate(pred, curr)) { if (curr.key == key) { pred.next = curr.next; return true; } else { return flase; } } } finally { pred.unlock (); curr.unlock(); } } }
  • 11.
    Optimistic Locking (2) publicboolean contains (T item) { int key = item.hashCode (); while (true) { Node pred = head; Node curr = pred.next; while (curr.key < key) { pred = curr; curr = curr.next; } try { pred.lock(); curr.lock (); if (validate(pred, curr)) { return (curr.key = key); } } finally { pred.unlock (); curr.unlock(); } } } public boolean validate ( Node pred, Node curr) { Node node = head; while (node.key <= pred.key) { if (node == pred) return pred.next = curr; node = node.next; } return false; }
  • 12.
  • 13.
    Lazy Synchronization • Postponesignificant changes, e.g., physically removing a node from a linked list • Allow a list to be traversed only once without locking, and contains() is wait-free. • Require a new field marked to be added into each node
  • 14.
    Lazy Locking (1) publicboolean add (T item) { int key = item.hashCode (); while (true) { Node pred = head; Node curr = pred.next; while (curr.key < key) { pred = curr; curr = curr.next; } pred.lock(); try { curr.lock(); try { if (validate(pred, curr)) { if (curr.key == key) { return false; } else { Node node = new Node (item); node.next = curr; pred.next = node; return true; } } } finally { curr.unlock(); } } finally { pred.unlock (); } } } public boolean remove (T item) { int key = item.hashCode (); while (true) { Node pred = head; Node curr = pred.next; while (curr.key < key) { pred = curr; curr = curr.next; } pred.lock(); try { curr.lock (); try { if (validate(pred, curr)) { if (curr.key != key) { return false; } else { curr.marked = true; pred.next = curr.next; return flase; } } } finally { curr.unlock (); } } finally { pred.unlock (); } } }
  • 15.
    Lazy Locking (2) publicboolean contains (T item) { int key = item.hashCode (); Node curr = head; while (curr.key < key) { curr = curr.next; } return curr.key == key && ! curr.marked; } private boolean validate (Node pred, Node curr) { return !pred.marked && !curr.marked && pred.next = curr; }
  • 16.
    Can we dobetter? • Non-blocking synchronization: eliminate locks entirely, relying on built-in atomic operations such as compareAndSet() for synchronization

Editor's Notes

  • #3 Nodes are sorted, so that it is quick to detect the absence of a node. We will assume the keys are unique.
  • #4 Use a single lock to manage all the synchronization operations.
  • #8 The problem is that there is no overlap between the locks held by the two remove() calls.
  • #9 This is because another thread could remove node b after you release pred but before you acquire curr.
  • #10 Problems with fine-grained locking: Still requires a lot of lock acquisitions and releases. Threads access disjoint items may still block each other, e.g., a thread removing the second item in the list blocks all concurrent threads searching for later nodes.
  • #12 Validation checks that pred is still reachable, and it still points to curr.
  • #13 It is possible that the trail of references leading to pred or the reference from pred to curr could have changed between when they were last read by a thread and when the thread acquires the locks (to perform the actual operation). Note that absence of interference implies that once a node has been unlinked from the list, the value of its next field does not change, so following a sequence of such links eventually leads back to the list.
  • #14 OptimisticLock works well if the cost of traversing the list twice without locking is significantly less than the cost of traversing the list once with locking. However, one drawback is that contains() are likely to be called very often, and it requires locks. Traversal does not require locking. Maintains the invariant that every unmarked node is reachable. Remove takes two steps: First, mark the target node, logically removing it, and second, redirect its predecessor’s next field, physically removing it.