一個線程可以共享它的預期修改來替代拷貝和修改內存中的整個數據結構.一個線程須要進行以下幾個操做來實現對一個共享數據的預期修改:html
如你所見,第二個操做提交一個預期修改會阻塞其餘線程.由於第二個操做實際上等同於做用在共享數據結構上的鎖.若是一個線程成功提交一個預期修改,那麼其餘線程將沒法提交預期修改,直到上一個提交的預期修改被執行爲止.java
若是一個線程提交一個預期修改後由於執行其餘任務而發生阻塞,那麼共享數據結構等同於鎖死.共享數據結構並不會直接阻塞其餘線程來使用它.其餘線程可以檢測到沒法提交預期修改而後再決定作些什麼.很明顯,咱們須要解決這種狀況.算法
爲了解決提交一個預期修改會鎖住共享數據結構的問題,一個提交的預期修改對象須要包含足夠的信息以讓其餘線程能夠繼續完成這些修改.這樣,當線程提交一個預期修改後沒法完成它時,其餘線程能夠經過它本身的方式來完成此次修改,同時讓共享數據結構能繼續被其餘線程使用.數據結構
下圖描述了上文給出的非阻塞算法設想:多線程
修改必須經過一到屢次cas操做來進行.所以,若是有兩個線程同時嘗試完成預期修改,只會有一個線程可以成功完成.併發
上文描述的算法中會遇到ABA問題.ABA問題是指一個變量從A更改到B,再從B被更改到A的時候,其餘線程沒法檢測到變量實際上已經被修改過了.工具
若是一個線程A先檢查是否有正在進行的更改,而後拷貝數據再而後就被線程調度器掛起了,此時線程B在同一時間訪問共享數據結構.此時若是線程B對共享數據執行了一個完整的修改,而後移除預期修改對象的引用,那麼對於線程A來講,它會誤覺得自從拷貝數據結構後預期修改並無被替換過.然而,預期修改的的確確已經被替換過了.當線程A基於過時的數據結構副本進行修改時,實際上內存中的數據結構已經被線程B的修改替換了.post
下圖描述了上文討論的ABA問題的場景:性能
一個通用的解決方案,不僅僅只是替換掉預期修改對象的指針,同時須要更新一個計數器,且替換預期修改對象指針和更新計數器須要在一個cas操做中完成.這在C和C++的指針中是可行的.所以,即便當前預期修改對象的指針被設置爲"沒有進行中的預期修改"的狀態,仍然會有一個計數器來記錄預期修改被更新的次數,以保障更新對其餘線程可見.學習
在Java中不能合併一個引用和計數器到一個變量中.但Java中提供了一個AtomicStampedReference對象用於完成在一個cas操做中同時替換引用和一個標記.
下面提供了一個代碼模版,這個模版提供了一個實現非阻塞算法的思路.這個模版基於上文給出的思路實現.
須要注意的是: 這份模版的做者並非一個專業非阻塞算法工程師,模版中可能會有幾處錯誤.因此告誡咱們千萬不要基於這個模版去實現本身的非阻塞算法.這個模版只是示例了非阻塞算法的實現代碼的思路.若是你須要實現一個非阻塞算法,那麼你須要研讀其餘更專業的書籍.須要瞭解一個非阻塞算法是如何實現和工做的,以及如何在實踐中編碼實現它.(如做者所說,他只是提供了一個思路,筆者在學習完這篇博文後總以爲做者提供非阻塞算法思路並不完整,因此看成入門資料是能夠,但要真正掌握非阻塞算法還有很長的路要走.)
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicStampedReference;
public class NonblockingTemplate {
public static class IntendedModification {
public AtomicBoolean completed =
new AtomicBoolean(false);
}
private AtomicStampedReference<IntendedModification> ongoingMod = new AtomicStampedReference<IntendedModification>(null, 0);
//declare the state of the data structure here.
public void modify() {
while(!attemptModifyASR());
}
public boolean attemptModifyASR(){
boolean modified = false;
IntendedModification currentlyOngoingMod = ongoingMod.getReference();
int stamp = ongoingMod.getStamp();
if(currentlyOngoingMod == null){
//copy data structure state - for use
//in intended modification
//prepare intended modification
IntendedModification newMod = new IntendedModification();
boolean modSubmitted = ongoingMod.compareAndSet(null, newMod, stamp, stamp + 1);
if(modSubmitted){
//complete modification via a series of compare-and-swap operations.
//note: other threads may assist in completing the compare-and-swap
// operations, so some CAS may fail
modified = true;
}
} else {
//attempt to complete ongoing modification, so the data structure is freed up
//to allow access from this thread.
modified = false;
}
return modified;
}
}
複製代碼
非阻塞算法很難被正確的設計和實現.在嘗試實現你本身的非阻塞算法前,不妨查看一下有沒有人已經實現過了.
Java中已經實現了一小部分非阻塞算法(例如ConcurrentLinkedQueue)且在將來會有更多的非阻塞算法實現加入到Java版本中.
除了Java中內建的一些非阻塞算法實現外,還有一些開源的數據結構可選.例如,LMAX Disrupter(一個相似隊列的數據結構)和由Cliff Click實現的非阻塞版本的HashMap.
對比阻塞算法,非阻塞算法可以給咱們帶來諸多好處.如下列出詳細的說明:
非阻塞算法帶來的第一個好處是: 線程的請求操做被拒絕時能夠選擇作些什麼而不是直接被阻塞掉.有時候線程的請求操做被拒絕後確實不知道應該作什麼.這個時候能夠選擇阻塞或是掛起來讓出CPU運行時間片去作其餘任務.但這至少給予了請求線程一次選擇的機會.
在單CPU的系統上,當線程的請求操做沒法被執行時將會被掛起以騰出CPU運行時間片來作其餘事情.可是,即便在單CPU的系統上,阻塞算法仍然會帶來死鎖,飢餓和其餘併發問題.
第二個好處是: 一個線程的掛起不會致使其餘線程的掛起.這意味着不會有死鎖發生.兩個線程不會互相等待對方釋放本身所須要的鎖.線程的請求操做不能被執行時不會發生阻塞,所以它們不須要阻塞以相互等待對方執行完成.非阻塞算法雖然不會發生死鎖,但會發生活鎖,兩個線程都在嘗試執行操做,但一直被告知這些操做不能執行(由於其餘線程正在操做的過程當中, 理論上是有可能發生的).
掛起和恢復一個線程的性能消耗是十分昂貴的.即便在操做系統和線程工具已經很是高效的狀況下,掛起和恢復線程對性能的消耗已經很小了.可是咱們仍然須要記住掛起和恢復一個線程是一筆不小的性能消耗(能避免則避免).
當一個線程被阻塞掛起時,須要消耗而外的性能來恢復它們.而在非阻塞算法中,線程不會掛起,這些性能消耗就不會發生.這意味着CPU有更多的運行時間片來執行真正的業務邏輯而不是線程的上下文切換.
在多線程系統中,阻塞算法會對程序的執行效率產生嚴重的影響.在CPU A上運行的線程可能會被阻塞以等待CPU B上運行的線程.這會下降應用程序的併發性.即便讓CPU A切換另一個線程來執行,線程間的上下文切換仍然是十分昂貴的.越少線程被掛起越好.
延遲在這裏是指一個線程發起請求操做到真正被執行的所通過的時間.線程在非阻塞算法中不會被掛起,所以它們沒有昂貴的恢復成本.這意味着當一個線程的請求操做可以被執行時,線程能夠快速響應從而最大程度的減小它們的響應延遲.
非阻塞算法一般能夠在請求操做真正可以被執行時經過繁忙等待的方式來取得最小的響應延遲.固然,若是一個線程在非阻塞數據結構上的競爭狀況比較激烈的話,那麼CPU會花費大量的運行時間片在繁忙等待上.因此咱們須要謹記,多個線程在數據結構上競爭狀況比較激烈的狀況下,非阻塞算法就顯得不是那麼合適了.然而,比較常見的作法是重構咱們的應用,讓線程儘可能少的爭奪內存中的數據結構.
該系列博文爲筆者複習基礎所著譯文或理解後的產物,複習原文來自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial