java併發庫(java.util.concurrent)提供了很多(相比鎖)性能更優越的同步設施,比如ConcurrentLinkedQueue。本章的主題,是研究此類併發裝置的性能祕密:原子變量和非阻塞同步。
鎖的性能劣勢
對現代JVM來說,在鎖未發生競爭的情況下,JVM執行“加鎖、釋放鎖”操作是非常快的;但是一旦發生競爭,就需要執行系統調用來掛起競爭失敗的線程,等將來鎖釋放時再喚醒它們。
掛起和喚醒線程的性能損耗是不能忽視的,對那些需要加鎖的高頻操作(比如集合讀寫)來說,鎖競爭導致的線程調度消耗的CPU,和業務邏輯操作消耗的CPU之間的比例,有可能會非常之高。換句話說,鎖競爭導致CPU一直在執行線程的調入&調出,花在執行業務代碼上的時間反而較少。
Volatile變量是一種輕量級的同步機制,不會阻塞線程;但Volatile不能保證變量操作的原子性,所以使用場景非常有限。
樂觀鎖
排他鎖是一種“悲觀“鎖,它總是假定“壞”的事情(競爭)會發生,在未獲得鎖之前,不能執行任何操作。對於細粒度的操作,還有一種偏“樂觀”的方式,它假定不會發生競爭,先嚐試執行操作再說;但是一旦發生衝突,需要能檢測到衝突。
爲了實現這種“樂觀”的同步機制,現代CPU都提供一種原子性的“read-modify-write”指令,最典型的是CAS(Compare and Swap)指令。JVM很早就使用了CAS指令,但一直到Java 5, java代碼才能使用它。
CAS指令
CAS指令有三個參數:內存地址、期望的原值、新值,它僅當內存地址存儲的值等於“期望的原值”,纔將它修改爲“新值”,無論成功或失敗,該指令返回“指令執行之前,內存地址存儲的值”。
CAS的語義,可以用下面的代碼來模擬:
@ThreadSafe
public class SimulatedCAS {
@GuardedBy("this") private int value;
public synchronized int get() { return value; }
//compareAndSwap模擬CAS指令
public synchronized int compareAndSwap(int expectedValue,int newValue) {
int oldValue = value;
if (oldValue == expectedValue)
value = newValue;
return oldValue;
}
//線程用cas執行同步操作,在失敗時能得到通知,不會阻塞
public synchronized boolean compareAndSet(int expectedValue,int newValue) {
return (expectedValue == compareAndSwap(expectedValue, newValue));
}
}
原子變量
原子變量是細粒度、輕量級的同步裝置,是CAS指令最直接的應用。可以認爲原子變量是更好的“Volatile”變量,它保留了Volatile變量的內存可見性保證,同時支持原子的“read-modify-write”操作。
Java的原子變量類型可分爲四組:
- 標量:AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference,LongAdder,LongAccumulator;
- field upaters:原子性地更新一個對象字段的工具,比如AtomicIntegerFieldUpdater;
- 數組:支持對數組元素執行原子操作,比如AtomicIntegerArray;
- 組合變量:AtomicStampedReference和AtomicMarkableReference,特殊用途。
Atomic變量最常用的操作就是compareAndSet,它內部調用了平臺的CAS操作。Atomic類型都是對java基本類型的包裹,因爲它只能爲非常細粒度的操作提供原子性;它無法爲多個狀態字段的操作提供原子性保證。AtomicStampedReference這種所謂的組合原子變量,是通過內嵌的不可變對象來組合多個字段,本質上就是個AtomicReference。
示例:CasNumberRange
現在用Atomic變量來實現一個線程安全的range結構:
public class CasNumberRange {
@Immutable
private static class IntPair {
final int lower; // Invariant: lower <= upper
final int 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();
IntPair newv = new IntPair(i, oldv.upper);
if (values.compareAndSet(oldv, newv))
return;
}
}
}
如果把lower和upper字段替換成Atomic變量是無法湊效的,因爲它們並不互相獨立。所以將它們封裝成一個不可變對象IntPair,修改時替換成新的IntPair實例。
AtomicStampedReference和AtomicMarkableReference的實現也是這個套路。
Atomic Field Updater
Atomic field updater可以用原子操作來修改對象的某個字段,比如ConcurrentLinkedQueue的內部Node結構類似如下:
private class Node<E> {
private final E item;
private volatile Node<E> next;
public Node(E item) {
this.item = item;
}
}
Node.next的更新需要原子性,但Node.next並不是Atomic變量,而是一個普通的volatile變量,ConcurrentLinkedQueue通過AtomicReferenceFieldUpdater來更新它:
private static AtomicReferenceFieldUpdater<Node, Node> nextUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "next");
Atomic Field Updater提供了對象字段的一個atomic操作工具,但是程序是有可能繞過updater來修改字段的,所以updater的原子性保證並不完整;因而,updater只能用來更新被完全封裝的內部對象字段。
使用updater的動機有兩個:
- 避免Atomic破壞對象的序列化;
- 減少一層對象,獲得微弱的性能優勢(如果next字段的類型是AtomicReference,相當於多了一層對象訪問)。
所以一般的情況下,我們是沒有必要使用updater的。
ABA問題
Atomic通過對比內存地址的期望值和實際值來判斷是否存在衝突。存在一種極限情況:線程在獲取舊值和置換新值兩個操作之間,有另外一個線程將舊值修改了兩次(先改成另外一個,又改回來),這就是所謂的ABA問題。在絕大多數情況下,ABA問題對業務沒有任何影響,我們可以忽略它。CAS操作本身無法避開ABA問題,Atomic提供了兩種類型來提供解決方案:AtomicStampedReference和AtomicMarkableReference,
AtomicStampedReference給reference加上一個int標記,每次修改,這個標記加1;AtomicMarkableReference則維護一個bool標記。
Atomic VS Lock
CAS指令消耗的CPU週期在10~150之間;而且在市場競爭的驅動下,處理器的CAS指令會越來越快。可以定性認爲,在都沒有發生競爭的情況下,Atomic的操作的速度大概是Lock加鎖解鎖的兩倍;而發生競爭的情況下,Lock會導致線程掛起,Atomic不會阻塞線程,二者不具備可比性,除非Atomic變量操作陷入了“活鎖“,否則性能遠優於Lock。
非阻塞同步算法&數據結構
基於Atomic變量或CAS操作,可以設計出非阻塞且線程安全的算法或數據結構,由於沒有阻塞,此類算法不會有死鎖風險。另一方面,由於它不會鎖住數據狀態然後再操作,而是任由多個線程併發地操作狀態,因此算法複雜度要大很多。
示例1:ConcurrentStack
現在我們使用Atomic來實現一個併發安全的Stack數據結構。
@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;
//如果有其他線程併發第push或pop,top會發生變化,compareAndSet操作會失敗
} 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;
//如果有其他線程併發第push或pop,top會發生變化,compareAndSet操作會失敗
} 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;
}
}
}
ConcurrentStack將元素放入Node,將Node組織爲一個單鏈表,只需要持有棧頂節點——top,就可以了。
ConcurrentStack.push操作步驟:
- 創建出新的棧頂節點newHead;
- 獲取當前棧頂的元素(oldHead),讓newHead.next=oldHead,維護了單向鏈表;
- 通過cas操作將top設置爲newHead
- 如果沒有併發競爭,那麼cas操作成功
- 如果發生競爭,那麼oldHead會過期,那麼cas操作會失敗,返回步驟2重試
ConcurrentStack.push的正確性源自:Atomic提供了和volatile一樣的內存可見性,且compareAndSet能檢測到併發衝突。ConcurrentStack.pop的操作步驟是類似的,不再贅述。
ConcurrentStack是一個極簡的併發安全數據結構,但是足以展示出此類算法的精髓,ConcurrentLinkedQueue、ConcurrentHashMap的實現原理是類似的,只不過更復雜罷了。
總結
基於硬件處理器的CAS指令,Atomic變量提供了細粒度的非阻塞的原子操作,基於Atomic,我們又可以構建很多非阻塞的併發安全數據結構(見java.util.concurrent)。相比鎖,Atomic變量和併發數據結構,具備優越的併發性能。但是我們也要意識到,非阻塞同步機制不是在任何情況下可以取代鎖,非阻塞同步只能保證細粒度的原子操作,它無法爲涉及多個狀態字段的操作提供原子性保證。一個併發數據結構是無法被鎖定的,線程永遠無法準確定判定一個併發集合是否empty,它的size是多少。