An
algorithm is said to be Non-Blocking, if a failure or suspension of any thread
cannot cause failure or suspension of another thread. 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.
Why do we need
Non-Blocking Algorithms?
When
more than one thread working on same data, we need to synchronize the access to the shared data between threads. What I mean is, provide shared data access to at
most one thread at any point of time. In addition to atomic access, the
changes done by one thread must be visible to other threads. In simple terms,
application should maintain following two properties.
a. Atomicity
b. Visibility
What is atomicity?
Only
one thread can able to access the shared resources at a time. You can achieve
this by using synchronization.
What is Visibility?
Shared data updated by one thread is available to other thread when it enters a
synchronized block protected by that same monitor (lock).
The
traditional approach to maintain above two properties is achieved by using
Synchronization. In synchronization, if any thread attempts to acquire a lock
that is already held by another thread, the thread will block until the lock is
free. While the thread is in blocking state, it don’t perform any useful task,
it stays idle, which is not acceptable in all situations.
Disadvantages of Lock
Based Approach
a. Contention: If one thread holds the lock on a resource,
then all the other threads that requested for this resource are blocked. If one
of the threads holding a lock dies, stalls, blocks, or enters an infinite loop,
other threads waiting for the lock may wait forever.
b. Race Condition: When writing multi-threaded applications,
one of the most common problems experienced are race conditions. A race
condition occurs when two or more threads can access shared data and they try
to change it at the same time.
For Example, Assume two threads T1, T2 tries
to update a counter. Assume counter has value 10, initially. Ideally,
programmer assumption is that the operations should execute in following
sequence.
Thread1
|
Thread2
|
Counter Value
|
10
|
||
Read
Counter Value
|
10
|
|
Increment
Counter Value by 1
|
10
|
|
Write
Back the value
|
11
|
|
Read
Counter Value
|
11
|
|
Increment
Counter Value by 1
|
11
|
|
Write
Back the value
|
12
|
If you don’t synchronize the operations
correctly, the outcome of the sequence can be wrong. For example, the outcome
can be 11, in following sequence.
Thread1
|
Thread2
|
Counter Value
|
10
|
||
Read
Counter Value
|
10
|
|
Read
Counter Value
|
10
|
|
Increment
Counter Value by 1
|
10
|
|
Increment
Counter Value by 1
|
10
|
|
Write
Back the value
|
11
|
|
Write
Back the value
|
11
|
c.
Debugging is
nightmare: It
is very difficult to reproduce the issue in synchronized environment.
d.
Scheduling Overhead: If multiple threads
blocked for a resource. Whenever the resource is released, there is a selection
overhead required while allocating the resource to one of these threads.
e.
Priority Inversion: A Low-priority thread
holding a lock can prevent high-priority threads from proceeding.
f. Convoying: All other threads have to wait if a thread
holding a lock is descheduled due to a time-slice interrupt or page fault.
g. DeadLock: Incorrect designs can leads to deadlock.
Unlike
blocking algorithms, Non-Blocking algorithms don’t suffer from above
disadvantages. Many researches claiming that, non-blocking algorithms are
efficient than lock based algorithms.
Most
of the Non-Blocking algorithms use Compare-and-Swap (CAP) approach to achieve
synchronization.
CAP (Compare And
Swap)
CAP
is an atomic instruction used in multi-threading to achieve synchronization. It
compares the contents of a memory location to a given value and, only if they
are the same, modifies the contents of that memory location to a given new
value. If the value of the variable is updated by another thread in mean time,
same procedure is repeated using the updated value, until the operation succeed.
Let’s
see an example of incrementing counter, using synchronization first. Later
example shows CAP version of the counter.
Counter.java
public class Counter { private int value; public Counter(int value){ this.value = value; } public synchronized int getValue() { return value; } public synchronized int increment() { return ++value; } public synchronized int decrement() { return --value; } }
You
can ask me why the getValue() method is synchronized, it is because,
synchronization ensures that the thread accessing the variable value, can see
the most updated value.
Now
let’s implement the same counter application using CAS approach. Following
steps summarize the CAS procedure.
Step 1: Read current value of
counter at location ‘V’ and temporarily save it in oldValue.
Step 2: Apply whatever the
operations you want to perform on oldValue, compute the new updated value, save
it in newValue.
Step 3: Atomically update the
location ’V’ to the newValue if the current value at location ‘V’ is matches to
oldValue. If the oldValue is not matches to the current value, repeat the steps
1 to 3. If the oldValue matches to the current value at the location ‘V’, then
atomically update the oldValue to newValue.
public class CASAtomicUpdate { private int value; public synchronized int getValue() { return value; } public synchronized boolean casAtomicUpdate(int expectedValue, int newValue) { if (value == expectedValue) { System.out.println(Thread.currentThread().getName() + " Updated the value to " + newValue); value = newValue; return true; } return false; } }
class WorkerThread implements Runnable { private CASAtomicUpdate atomicUpdate; WorkerThread(CASAtomicUpdate atomicUpdate) { this.atomicUpdate = atomicUpdate; } public void run() { try { Thread.sleep(1000); Thread t1 = Thread.currentThread(); int oldValue, newValue; do { oldValue = atomicUpdate.getValue(); newValue = oldValue + 1; System.out .println(t1.getName() + " is trying to update the value from " + oldValue + " to " + newValue); } while (!atomicUpdate.casAtomicUpdate(oldValue, newValue)); } catch (Exception e) { System.out.println(e); } } }
import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class CASAtomicUpdateTest { public static void main(String args[]) throws InterruptedException { CASAtomicUpdate atomicCounter = new CASAtomicUpdate(); ExecutorService exec = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); WorkerThread tasks[] = new WorkerThread[10]; for (int i = 0; i < 10; i++) { tasks[i] = new WorkerThread(atomicCounter); exec.execute(tasks[i]); } } }
Sample output
pool-1-thread-2 is trying to update the value from 0 to 1 pool-1-thread-6 is trying to update the value from 0 to 1 pool-1-thread-2 Updated the value to 1 pool-1-thread-8 is trying to update the value from 0 to 1 pool-1-thread-8 is trying to update the value from 1 to 2 pool-1-thread-9 is trying to update the value from 1 to 2 pool-1-thread-7 is trying to update the value from 0 to 1 pool-1-thread-5 is trying to update the value from 0 to 1 pool-1-thread-4 is trying to update the value from 0 to 1 pool-1-thread-1 is trying to update the value from 0 to 1 pool-1-thread-3 is trying to update the value from 0 to 1 pool-1-thread-8 Updated the value to 2 pool-1-thread-10 is trying to update the value from 1 to 2 pool-1-thread-9 is trying to update the value from 2 to 3 pool-1-thread-6 is trying to update the value from 1 to 2 pool-1-thread-9 Updated the value to 3 pool-1-thread-10 is trying to update the value from 2 to 3 pool-1-thread-7 is trying to update the value from 2 to 3 pool-1-thread-5 is trying to update the value from 2 to 3 pool-1-thread-1 is trying to update the value from 2 to 3 pool-1-thread-4 is trying to update the value from 2 to 3 pool-1-thread-3 is trying to update the value from 2 to 3 pool-1-thread-4 is trying to update the value from 3 to 4 pool-1-thread-1 is trying to update the value from 3 to 4 pool-1-thread-5 is trying to update the value from 3 to 4 pool-1-thread-7 is trying to update the value from 3 to 4 pool-1-thread-10 is trying to update the value from 3 to 4 pool-1-thread-6 is trying to update the value from 3 to 4 pool-1-thread-4 Updated the value to 4 pool-1-thread-3 is trying to update the value from 3 to 4 pool-1-thread-5 is trying to update the value from 4 to 5 pool-1-thread-7 is trying to update the value from 4 to 5 pool-1-thread-10 is trying to update the value from 4 to 5 pool-1-thread-6 is trying to update the value from 4 to 5 pool-1-thread-5 Updated the value to 5 pool-1-thread-1 is trying to update the value from 4 to 5 pool-1-thread-7 is trying to update the value from 5 to 6 pool-1-thread-3 is trying to update the value from 4 to 5 pool-1-thread-7 Updated the value to 6 pool-1-thread-1 is trying to update the value from 5 to 6 pool-1-thread-10 is trying to update the value from 5 to 6 pool-1-thread-6 is trying to update the value from 5 to 6 pool-1-thread-10 is trying to update the value from 6 to 7 pool-1-thread-1 is trying to update the value from 6 to 7 pool-1-thread-3 is trying to update the value from 6 to 7 pool-1-thread-10 Updated the value to 7 pool-1-thread-6 is trying to update the value from 6 to 7 pool-1-thread-3 is trying to update the value from 7 to 8 pool-1-thread-1 is trying to update the value from 7 to 8 pool-1-thread-3 Updated the value to 8 pool-1-thread-6 is trying to update the value from 7 to 8 pool-1-thread-1 is trying to update the value from 8 to 9 pool-1-thread-6 is trying to update the value from 8 to 9 pool-1-thread-1 Updated the value to 9 pool-1-thread-6 is trying to update the value from 9 to 10 pool-1-thread-6 Updated the value to 10
In
the above example, I created 10 threads, each thread tries ti increment the
value of CASAtomicUpdate instance. In worst case, a thread retry at most nine
times before the increment is complete. As you observe the output, pool-1-thread-6
attempted 9 times to succeed its incremental operations.
pool-1-thread-6 is trying to update the value from 0 to 1 pool-1-thread-6 is trying to update the value from 1 to 2 pool-1-thread-6 is trying to update the value from 3 to 4 pool-1-thread-6 is trying to update the value from 4 to 5 pool-1-thread-6 is trying to update the value from 5 to 6 pool-1-thread-6 is trying to update the value from 6 to 7 pool-1-thread-6 is trying to update the value from 7 to 8 pool-1-thread-6 is trying to update the value from 8 to 9 pool-1-thread-6 is trying to update the value from 9 to 10 pool-1-thread-6 Updated the value to 10
Non-blocking
algorithms are difficult to implement, but they had many advantages like you
don’t get any deadlock situation, priority inversion never occur and contention
is less expensive.
Reference
No comments:
Post a Comment