思維導圖:
引言:
在此之前,所有對併發競爭的處理都是使用鎖來處理的。使用鎖固然可以保證修改的正確性,但是,所也有它固有的缺點。鎖的缺點如下:
- 高開銷:使用鎖必然意味着在多個線程競爭時大部分線程都會陷入沉睡然後喚醒的循環,會有極大的開銷。
- 長等待:等待中的線程是什麼也不能做的,只能等待,這是一種資源的浪費。
- 優先級反轉:當低優先級的線程獲取到鎖後,如果此時高優先級的線程也需要鎖,那也必須等待低優先級線程釋放纔行。
我們可以不使用鎖來構建併發程序嗎?當然是可以的。這也就是本章的主要內容,使用非阻塞同步機制來構建併發程序。但是,構建非阻塞同步機制來構建併發程序是一件非常複雜的事情,所以,最好還是使用已有的封裝類。
使用非阻塞同步機制的核心在於使用原子變量。接下來,本章會分爲以下兩個部分進行介紹:
- 原理部分:介紹原子變量的簡單使用。
- 使用部分:會介紹一些簡單的使用非阻塞同步機制構建的數據結構和算法。
一.原子變量類
非阻塞算法在在可伸縮性和活躍性上對比於鎖的獨佔式訪問有巨大的性能優勢。這些優勢來源於多個線程在競爭相同的數據時不會發生阻塞,他能夠在更細粒度的層次上進行協調,並且極大的減少調度開銷。這在java中是通過原子變量類實現的。接下來,在這個小節中會簡要的介紹一下原子變量類。
1.1 比較並交換
首先,第一個問題,非阻塞算法是如何實現在沒有阻塞的情況下解決多個線程對同一個數據的競爭的呢?答案就是比較並交換操作。其原理是線程A嘗試修改值得時候會檢查 此值是否正在被別的線程修改。如果當前值正在被別的線程修改,那麼此次更新操作失敗,然後,此線程會繼續嘗試更新,直到成功爲止。
比較並交換操作也稱爲CAS。在處理器中有專門的用於比較並交換操作的指令。其執行邏輯如下:當前需要讀寫的內存位置爲V,進行比較的值A,需要新寫入的值B。只有當V的值等於A的值時,纔會將內存V的位置的值更新爲B,否則不執行任何操作。
我們用java代碼來簡單模擬一下一上邏輯處理:
@ThreadSafe
public class SimulatedCAS {
@GuardedBy("this") private int value;
public synchronized int get() {
return value;
}
public synchronized int compareAndSwap(int expectedValue, int newValue) {
int oldValue = value;
if (oldValue == expectedValue) {
value = newValue;
}
return oldValue;
}
public synchronized boolean compareAndSet(int expectedValue, int newValue) {
return (expectedValue == compareAndSwap(expectedValue, newValue));
}
}
當然,在JVM中我們可以使用java.util.concurrent.atomic包中的AtomicXXX等原子變量類來進行上述操作,原子變量類已經將CAS操作封裝好了。
1.2 更好的volatile
在以前,我們使用不可變對象和volatile來維持包含多個變量的不變性條件,現在,我們可以使用原子變量類來維護不變性條件了。代碼如下:
@ThreadSafe
public class CasNumberRange {
@Immutable private static class IntPair {
// 不變性條件: lower <= upper
final int lower;
final int upper;
public IntPair(int lower, int upper) {
this.lower = lower;
this.upper = upper;
}
}
private final AtomicReference<IntPair> values = new AtomicReference<IntPair>(new IntPair(0, 0));
public int getLower() {
return values.get().lower;
}
public int getUpper() {
return values.get().upper;
}
public void setLower(int i) {
while (true) {
IntPair oldv = values.get();
if (i > oldv.upper) {
throw new IllegalArgumentException("Can't set lower to " + i + " > upper");
}
IntPair newv = new IntPair(i, oldv.upper);
if (values.compareAndSet(oldv, newv)) {
return;
}
}
}
public void setUpper(int i) {
while (true) {
IntPair oldv = values.get();
if (i < oldv.lower) {
throw new IllegalArgumentException("Can't set upper to " + i + " < lower");
}
IntPair newv = new IntPair(oldv.lower, i);
if (values.compareAndSet(oldv, newv)) {
return;
}
}
}
}
1.3 與鎖的性能比較
通過以下記這個念頭可以看到,在高度競爭的情況下,鎖的性能會超過原子變量的性能。但是在更真實的競爭情況下,不會有那麼高烈度的競爭,原子變量的性能會超過的鎖的性能。
二.非阻塞算法
在這個小節中,我們將會看到一些例子,是關於如何使用原子變量構建非阻塞算法的。這個算法不僅是隻有算法,還包括一些數據結構。一般來說,構建非阻塞算法是一件非常複雜的事,不推薦自己構建非阻塞的數據結構。
2.1 非阻塞計數器
以下代碼是一個非阻塞計數器,其中SimulatedCAS類已在上文中實現
@ThreadSafe
public class CasCounter {
private SimulatedCAS value;
public int getValue() {
return value.get();
}
public int increment() {
int v;
do {
v = value.get();
} while (v != value.compareAndSwap(v, v + 1));
return v + 1;
}
}
2.2 非阻塞棧
@ThreadSafe
public class ConcurrentStack <E> {
AtomicReference<Node<E>> top = new AtomicReference<Node<E>>();
public void push(E item) {
Node<E> newHead = new Node<E>(item);
Node<E> oldHead;
do {
oldHead = top.get();
newHead.next = oldHead;
} while (!top.compareAndSet(oldHead, newHead));
}
public E pop() {
Node<E> oldHead;
Node<E> newHead;
do {
oldHead = top.get();
if (oldHead == null) {
return null;
}
newHead = oldHead.next;
} while (!top.compareAndSet(oldHead, newHead));
return oldHead.item;
}
private static class Node <E> {
public final E item;
public Node<E> next;
public Node(E item) {
this.item = item;
}
}
}
2.3 非阻塞鏈表
構建非阻塞鏈表的時候我們會遇見一個問題,即我們需要在一個步驟中包含多個更新操作,類似於事務,以保證數據的一致性。
我們一般用兩個技巧來解決這一問題:
- 計時在一個包含多個步驟的更新操作中,也要確保數據結構總是處於一致的狀態。
- 如果當B到達時發現A正在修改數據結構,那麼在數據結構中因該有足夠的信息,使得B能夠完成A的更新操作。
以鏈表的put操作舉例,當我們需要put一個新的數據時,需要進行兩步的操作,一是將以前的尾節點的next引用指向當前節點,二是將尾節點指向當前節點。
更新時可能會發生這樣的情況,線程A操作一成功了,操作二失敗了。此時數據的一致性失效了,數據結構是不穩定的。此時若線程B也進行了更新操作。就需要判斷數據結構是否處於穩定的狀態,如果不穩定,就要讓他變得穩定。對於鏈表來說,是否穩定的判斷標準就是尾節點是否執行真正的末尾節點。如下圖所示:
如果處於不穩定的狀態,那麼線程B可以先將線程A的操作執行完畢,然後再來執行自己的操作。代碼如下所示:
@ThreadSafe
public class LinkedQueue <E> {
private static class Node <E> {
final E item;
final AtomicReference<Node<E>> next;
public Node(E item, Node<E> next) {
this.item = item;
this.next = new AtomicReference<Node<E>>(next);
}
}
private final Node<E> dummy = new Node<E>(null, null);
private final AtomicReference<Node<E>> head = new AtomicReference<Node<E>>(dummy);
private final AtomicReference<Node<E>> tail = new AtomicReference<Node<E>>(dummy);
public boolean put(E item) {
Node<E> newNode = new Node<E>(item, null);
while (true) {
Node<E> curTail = tail.get();
Node<E> tailNext = curTail.next.get();
if (curTail == tail.get()) {
if (tailNext != null) {
// 隊列處於中間狀態,推進尾節點
tail.compareAndSet(curTail, tailNext);
} else {
// 處於穩定狀態,嘗試插入新節點
if (curTail.next.compareAndSet(null, newNode)) {
// 插入成功,嘗試推進尾節點
tail.compareAndSet(curTail, newNode);
return true;
}
}
}
}
}
}