This document discusses different locking techniques for concurrent data structures:
- Coarse-grained locking locks the entire data structure for each operation, limiting concurrency.
- Fine-grained locking locks individual nodes to improve concurrency but requires per-node locks.
- Optimistic locking performs operations lock-free until validation, when it locks affected nodes.
- Lazy locking delays changes like removal by marking nodes instead of immediately changing pointers, allowing lock-free traversal.
3. 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
4. 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 ();
}
5. 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
6. 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 ();
}
}
9. 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
10. 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();
}
}
}
11. 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;
}
13. 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
14. 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 ();
}
}
}
16. Can we do better?
• Non-blocking synchronization: eliminate locks
entirely, relying on built-in atomic operations
such as compareAndSet() for synchronization
Editor's Notes
Nodes are sorted, so that it is quick to detect the absence of a node. We will assume the keys are unique.
Use a single lock to manage all the synchronization operations.
The problem is that there is no overlap between the locks held by the two remove() calls.
This is because another thread could remove node b after you release pred but before you acquire curr.
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.
Validation checks that pred is still reachable, and it still points to curr.
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.
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.