java多線程11.非阻塞同步機制
關於非阻塞算法CAS。 比較並交換CAS:CAS包含了3個操作數---需要讀寫的內存位置V,進行比較的值A和擬寫入的新值B。當且僅當V的值等於A時,CAS才會通過原子的方式用新值B來更新V的值,否則不會執行任何操作。無論位置V的值是否等於A,都將返回V原有的值。然後線程可以基於新返回的V值來做對應的操作,可以反復嘗試。通常,反復重試是一種合理的策略,但在一些競爭很激烈的情況下,更好的方式是在重試之前首先等待一段時間或者回退,從而避免造成活鎖問題。CAS的主要缺點就是,它將使調用者處理競爭問題,而在鎖中能自動處理競爭問題。雖然java語言的鎖定語句比較簡潔,但JVM和操作在管理鎖時需要完成的工作卻並不簡單。在實現鎖定時需要遍歷JVM中一條非常復雜的代碼路徑,並可能導致操作系統級的鎖定、線程掛起以及上下文卻換等動作。在最好的情況下,在鎖定時至少需要一次CAS,因此雖然在使用鎖時沒有用到CAS,但實際上也無法節約任何執行開銷。另外,在程序內部執行CAS不需要執行JVM代碼、系統調用或線程調度操作。在應用級上看起來越長的代碼路徑,如果加上JVM和操作系統中的代碼調用,那麽事實上卻變得更短。
在非阻塞算法中不存在死鎖和其他活躍性問題。
而在基於鎖的算法中,如果一個線程在休眠或自旋的同時持有一個鎖,那麽其他線程都無法執行下去,而非阻塞算法不會受到單個線程失敗的影響。
鎖的劣勢
許多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把這個新節點放入棧頂。 * * @param <E> */ 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已經替它完成了。
java多線程11.非阻塞同步機制