StaticPL/JAVA
Non-blocking, Lock-free operations
데먕
2020. 2. 27. 17:50
1. Overview
In computer science, an algorithm is called non-blocking if failure or suspension of any thread cannot cause failure or suspension of another thread; for some operations, these algorithms provide a useful alternative to traditional blocking implementations. A non-blocking algorithm is lock-free if there is guaranteed system-wide progress, and wait-free if there is also guaranteed per-thread progress.
1.1 DealLocks
- Deadlocks are generally unrecoverable
- Can bring the application to a complete halt
- The more locks in the application, the higher the chances for a deadlock
1.2 Slow Critical Section
- Multiple threads using the same lock
- One thread holds the lock for very long
- That thread will slow down all the other threads
- All threads become as slow as the slowest thread
1.3 Priority Inversion
- Two threads sharing a lock
- Low priority thread (document server)
- Hight priority thread (UI)
- Low priority thread acquires the lock and is preempted (scheduled out)
- High priority thread cannot progress because of the low priority thread is not scheduled to release the lock
1.4 Thread not Releasing a lock (Kill Tolerance)
- Thread dies, gets interrupted or forget to release a lock
- Leaves all thread hanging forever
- Unrecoverable, Just like a deadlock
- To avoid, developers need to write more complex code
1.5 Performance
- Performance overhead in having contention over a lock
- Thread A acquires a lock
- Thread B tries to acquire a lock and gets blocked
- Thread B is scheduled out (context switch)
- Thread B is scheduled back (context switch)
- Additional overhead may not be noticeable for most applications
- But for latency-sensitive applications, this overhead can be significant
1.6 Why did we need locks?
- Multiple threads accessing shared resources
- At least one thread is modifying the shared resources
- Operations are not atomic
- A single Java operation turns into one or more hardware operations
- Example: counter++ turns into 3 hardware instructions:
- Read count
- Calculate new value
- Store a new value to count
2. Description
2.1 Lock Free solution
2.1.1 Breakdown
- Utilize operations which are guaranteed to be one hardware operation
- A single hardware operation is
- Atomic by definition
- Thread-safe
2.1.2 Atomic instructions
- Read/Assignment on all primitive types except for long and double
- Read/Assignment on all references
- Read/Assignment on volatile long and double
2.1.3 Avoiding Data Races
- Read/Assignment on all volatile primitive types and references
2.1.4 AtomicX Classes
- Class located in the java.util.concurrent.atomic package
- Internally uses the Unsafe Class which provides access to a low level, native methods
- Utilize platform-specific implementation of atomic operations
3. Example
3.1 Stack
3.1.1 With synchronized keyword
public static class StandardStack<T> {
private StackNode<T> head;
private int counter = 0;
public synchronized void push(T value) {
StackNode<T> newHead = new StackNode<>(value);
newHead.next = head;
head = newHead;
counter++;
}
public synchronized T pop() {
if(head == null) {
counter++;
return null;
}
T value = head.value;
head = head.next; // previous head is garbage collected automatically by GC
counter++;
return value;
}
public int getCounter() {return counter;}
}
private static class StackNode<T> {
public T value;
public StackNode<T> next;
public StackNode(T value) {
this.value = value;
this.next = next;
}
}
3.1.2 With the atomic package
public static class LockFreeStack<T> {
private AtomicReference<StackNode<T>> head = new AtomicReference<StackNode<T>>();
private AtomicInteger counter = new AtomicInteger(0);
public void push(T value) {
StackNode<T> newHeadNode = new StackNode<>(value);
while (true) {
StackNode<T> currentHeadNode = head.get();
newHeadNode.next = currentHeadNode;
if(head.compareAndSet(currentHeadNode, newHeadNode)) {
break;
} else {
LockSupport.parkNanos(1);
}
}
counter.incrementAndGet();
}
public T pop() {
StackNode<T> currentHeadNode = (StackNode<T>) head.get();
StackNode<T> newHeadNode;
while(currentHeadNode != null) {
newHeadNode = currentHeadNode.next;
if(head.compareAndSet(currentHeadNode, newHeadNode)) {
break;
} else {
LockSupport.parkNanos(1);
currentHeadNode = (StackNode<T>) head.get();
}
}
counter.incrementAndGet();
return currentHeadNode != null ? currentHeadNode.value : null;
}
public int getCounter() {
return counter.get();
}
}
// synchronized
190,568,501 operations were performed in 10 seconds
// atomic
743,488,968 operations were performed in 10 seconds
3.2 Arithmetic
3.2.1 Using AtomicReference wrapping primitive
private static class Metric {
private static class InternalMetric{
public long count;
public long sum;
}
AtomicReference<InternalMetric> internalMetric = new AtomicReference<>(new InternalMetric());
public void addSample(long sample) {
InternalMetric currentState;
InternalMetric newState;
do {
currentState = internalMetric.get();
newState = new InternalMetric();
newState.sum = currentState.sum + sample;
newState.count = currentState.count + 1;
} while (!internalMetric.compareAndSet(currentState, newState));
}
public double getAverage() {
InternalMetric newResetState = new InternalMetric();
InternalMetric currentState;
double average;
do {
currentState = internalMetric.get();
average = (double)currentState.sum / currentState.count;
} while (!internalMetric.compareAndSet(currentState, newResetState));
return average;
}
}
4. Reference
https://en.wikipedia.org/wiki/Non-blocking_algorithm
https://www.justsoftwaresolutions.co.uk/threading/non_blocking_lock_free_and_wait_free.html