標籤:原子變數 都對 nod 定時 push value 部分 mic 中斷
在非阻塞演算法中不存在死結和其他活躍性問題。
在基於鎖的演算法中,如果一個線程在休眠或自旋的同時持有一個鎖,那麼其他線程都無法執行下去,而非阻塞演算法不會受到單個線程失敗的影響。
鎖的劣勢
許多JVM都對非競爭鎖擷取和釋放操作進行了極大的最佳化,但如果有多個線程同時請求鎖,那麼JVM就需要藉助作業系統地功能。如果出現了這種情況,那麼一些線程將被掛起並且在稍後恢複運行。當線程恢複執行時,必須等待其他線程執行完它們的時間片以後,才能被調度執行。在掛起和恢複線程等過程中存在著很大的開銷,並且通常存在著較大時間的中斷。如果在基於鎖的類中包含細粒度的操作(例如同步器類,在其大多數方法中只包含了少量操作),那麼當在鎖上存在著激烈的競爭時,調度開銷與工作開銷的比值會非常高。
另外,當一個線程正在等待鎖時,它不能做任何其他事情。如果一個線程在持有鎖的情況下被順延強制,那麼所有需要這個鎖的線程都無法執行下去。如果被阻塞線程的優先順序高,而持有鎖的線程優先順序低,那麼將是一個嚴重的問題。
比較並交換CAS
CAS包含了3個運算元---需要讀寫的記憶體位置V,進行比較的值A和擬寫入的新值B。若且唯若V的值等於A時,CAS才會通過原子的方式用新值B來更新V的值,否則不會執行任何操作。無論位置V的值是否等於A,都將返回V原有的值。
CAS的含義:我認為V的值應該為A,如果是,那麼將V的值更新為B,否則不修改並告訴V的值實際為多少。CAS是一種樂觀的態度,它希望能成功地執行更新操作,並且如果有另一個線程在最近一次檢查後更新了該變數,那麼CAS能檢測到這個錯誤。
/** * 當多個線程嘗試使用CAS同時更新同一個變數時,只有其中一個線程能更新變數的值,而其他線程都將失敗。 * 然而,失敗的線程並不會被掛起,而是被告知在這次競爭中失敗,並可以再次嘗試。 * 由於一個線程在競爭CAS時不會阻塞,因此它可以決定是否重新嘗試,或者執行一些恢複操作,也或者不執行任何操作。 */public class SimulatedCAS { 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)); }}
CAS的典型使用模式是:首先從V中讀取值A,並根據A計算新值B,然後再通過CAS以原子方式將V中的值由A變成B。由於CAS能檢測到來自其他線程的幹擾,因此即使不使用鎖也能夠實現原子的讀--改--寫操作。
非阻塞的計算機
/** * 通常,反覆重試是一種合理的策略,但在一些競爭很激烈的情況下, * 更好的方式是在重試之前首先等待一段時間或者回退,從而避免造成活鎖問題。 * * 雖然java語言的鎖定語句比較簡潔,但JVM和操作在管理鎖時需要完成的工作卻並不簡單。 * 在實現鎖定時需要遍曆JVM中一條非常複雜的代碼路徑,並可能導致作業系統級的鎖定、線程掛起以及上下文卻換等動作。 * 在最好的情況下,在鎖定時至少需要一次CAS,因此雖然在使用鎖時沒有用到CAS,但實際上也無法節約任何執行開銷。 * 另外,在程式內部執行CAS不需要執行JVM代碼、系統調用或線程調度操作。 * 在應用級上看起來越長的代碼路徑,如果加上JVM和作業系統中的代碼調用,那麼事實上卻變得更短。 * CAS的主要缺點是,它將使調用者處理競爭問題,而在鎖中能自動處理競爭問題 * */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; }}
JVM對CAS的支援
Java5.0中引入了底層的支援,在int,long和對象引用等類型上都公開了CAS操作,並且JVM把它們編譯為底層硬體提供的最有效方法。在原子變數類中,使用了這些底層的JVM支援為數字類型和參考型別提供一種高效的CAS操作,而在java.util.concurrent中的大多數類在實現時都直接或間接地使用了這些原子變數類。
非阻塞的棧
/** * 棧是由Node元素構成的一個鏈表,根節點為棧頂yop,每個元素中都包含了一個值以及指向下一個元素的連結。 * push方法建立一個新的節點,該節點的next域指向當前的棧頂,然後使用CAS把這個新節點放入棧頂。 */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> newHead; Node<E> oldHead; 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; } }}
非阻塞鏈表
鏈表隊列比棧複雜,它必須支援對前端節點和尾節點的快速存取。它需要單獨維護頭指標和尾指標。
對於尾部的插入,有兩個點需要更新:將當前尾節點的next指向要插入的節點,和將尾節點更新為新插入的節點。這兩個更新操作需要不同的CAS操作,不好通過原子變數來實現。需要使用一些策略:
策略一是,即使在一個包含多個步驟的更新操作中,也要確保資料結構總是處於抑制的狀態。這樣,線程B到達時,如果發現A正在執行更新,那麼線程B就可以知道有一個操作已部分完成,並且不能立即執行自己的更新操作。然後B可以等待並直到A完成更新。雖然能使不同的線程輪流訪問資料結構,並且不會造成破壞,但如果有一個線程在更新操作中失敗了,那麼其他的線程都無法再方位隊列。
策略二是,如果B到達時發現A正在修改資料結構,那麼在資料結構中應該有足夠多的資訊,使得B能完成A的更新操作。如果B協助A完成了更新操作,那麼B可以執行自己的操作,而不用等待A的操作完成。當A恢複後再試圖完成其操作時,會發現B已經替它完成了。
/** * 實現的關鍵點在於: * 當隊列處於穩定點時,未節點的next域將為空白,如果隊列處於中間狀態,那麼tail.next將為非空。 * 因此,任何線程都能夠通過檢查tail.next來擷取隊列當前的狀態。 * 而且,當隊列處於中間狀態時,可以通過將尾節點向前移動一個節點, * 從而結束其他線程正在執行的插入元素操作,並使得隊列恢複為穩定點。 */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); private 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; } } } } } }}
ABA問題
在某些演算法中,如果V的值首先由A變成B,再由B變成A。
解決辦法是:不是更新某個引用的值,而是更新兩個值,包括一個引用和一個版本號碼。AtomicStampedReference以及AtomicMarkableReference支援在兩個變數上執行原子的條件更新。
#筆記內容來自 《 java並發編程實戰》
java並發編程11.原子變數與非阻塞同步機制