1. 程式人生 > >資料庫事務與鎖詳解\何謂悲觀鎖與樂觀鎖

資料庫事務與鎖詳解\何謂悲觀鎖與樂觀鎖

https://blog.csdn.net/aluomaidi/article/details/52460844

什麼是事務(Transaction)?

是指作為單個邏輯工作單元執行的一系列操作,要麼完全地執行,要麼完全地不執行。 事務處理可以確保除非事務性單元內的所有操作都成功完成,否則不會永久更新面向資料的資源。通過將一組相關操作組合為一個要麼全部成功要麼全部失敗的單元,可以簡化錯誤恢復並使應用程式更加可靠。一個邏輯工作單元要成為事務,必須滿足所謂的ACID(原子性、一致性、隔離性和永續性)屬性。事務是資料庫執行中的一個邏輯工作單位,由DBMS中的事務管理子系統負責事務的處理。

舉個例子加深一下理解:同一個銀行轉賬,A轉1000塊錢給B,這裡存在兩個操作,一個是A賬戶扣款1000元,兩一個操作是B賬戶增加1000元,兩者就構成了轉賬這個事務。

  • 兩個操作都成功,A賬戶扣款1000元,B賬戶增加1000元,事務成功
  • 兩個操作都失敗,A賬戶和B賬戶金額都沒變,事務失敗

最後思考一下,怎麼樣會出現A賬戶扣款1000元,B賬戶金額不變?如果你是把兩個操作放在一個事務裡面,並且是資料庫提供的內在事務支援,那就不會有問題,但是開發人員把兩個操作放在兩個事務裡面,而第二個事務失敗就會出現中間狀態。現實中自己實現的分散式事務處理不當也會出現中間狀態,這並不是事務的錯,事務本身就是規定不會出現中間狀態,是事務實現者做出來的方案有問題。

事務的4個特性

  • 原子性(Atomic):事務必須是原子工作單元;對於其資料修改,要麼全都執行,要麼全都不執行。通常,與某個事務關聯的操作具有共同的目標,並且是相互依賴的。如果系統只執行這些操作的一個子集,則可能會破壞事務的總體目標。原子性消除了系統處理操作子集的可能性。

  • 一致性(Consistency):事務的一致性指的是在一個事務執行之前和執行之後資料庫都必須處於一致性狀態。這種特性稱為事務的一致性。假如資料庫的狀態滿足所有的完整性約束,就說該資料庫是一致的。

  • 隔離性(Isolation):由併發事務所作的修改必須與任何其它併發事務所作的修改隔離。事務檢視資料時資料所處的狀態,到底是另一個事務執行之前的狀態還是中間某個狀態,相互之間存在什麼影響,是可以通過隔離級別的設定來控制的。

  • 永續性(Durability):事務結束後,事務處理的結果必須能夠得到固化,即寫入資料庫檔案中即使機器宕機資料也不會丟失,它對於系統的影響是永久性的。

事務併發控制

我們從另外一個方向來說說,如果不對事務進行併發控制,我們看看資料庫併發操作是會有那些異常情形,有些使我們可以接受的,有些是不能接受的,注意這裡的異常就是特定語境下的,並不一定就是錯誤什麼的。假設有一個order表,有個欄位叫count,作為計數用,當前值為100

  • 第一類丟失更新(Update Lost):此種更新丟失是因為回滾的原因,所以也叫回滾丟失。此時兩個事務同時更新count,兩個事務都讀取到100,事務一更新成功並提交,count=100+1=101,事務二出於某種原因更新失敗了,然後回滾,事務二就把count還原為它一開始讀到的100,此時事務一的更新就這樣丟失了。

  • 髒讀(Dirty Read):此種異常時因為一個事務讀取了另一個事務修改了但是未提交的資料。舉個例子,事務一更新了count=101,但是沒有提交,事務二此時讀取count,值為101而不是100,然後事務一出於某種原因回滾了,然後第二個事務讀取的這個值就是噩夢的開始。

  • 不可重複讀(Not Repeatable Read):此種異常是一個事務對同一行資料執行了兩次或更多次查詢,但是卻得到了不同的結果,也就是在一個事務裡面你不能重複(即多次)讀取一行資料,如果你這麼做了,不能保證每次讀取的結果是一樣的,有可能一樣有可能不一樣。造成這個結果是在兩次查詢之間有別的事務對該行資料做了更新操作。舉個例子,事務一先查詢了count,值為100,此時事務二更新了count=101,事務一再次讀取count,值就會變成101,兩次讀取結果不一樣。

  • 第二類丟失更新(Second Update Lost):此種更新丟失是因為更新被其他事務給覆蓋了,也可以叫覆蓋丟失。舉個例子,兩個事務同時更新count,都讀取100這個初始值,事務一先更新成功並提交,count=100+1=101,事務二後更新成功並提交,count=100+1=101,由於事務二count還是從100開始增加,事務一的更新就這樣丟失了。

  • 幻讀(Phantom Read):幻讀和不可重複讀有點像,只是針對的不是資料的值而是資料的數量。此種異常是一個事務在兩次查詢的過程中資料的數量不同,讓人以為發生幻覺,幻讀大概就是這麼得來的吧。舉個例子,事務一查詢order表有多少條記錄,事務二新增了一條記錄,然後事務一查了一下order表有多少記錄,發現和第一次不一樣,這就是幻讀。

資料庫事務隔離級別

看到上面提到的幾種問題,你可能會想,我擦,這麼多坑怎麼辦啊。其實上面幾種情況並不是一定都要避免的,具體看你的業務要求,包括你資料庫的負載都會影響你的決定。不知道大家發現沒有,上面各種異常情況都是多個事務之間相互影響造成的,這說明兩個事務之間需要某種方式將他們從某種程度上分開,降低直至避免相互影響。這時候資料庫事務隔離級別就粉墨登場了,而資料庫的隔離級別實現一般是通過資料庫鎖實現的。

  • 讀未提交(Read Uncommitted):該隔離級別指即使一個事務的更新語句沒有提交,但是別的事務可以讀到這個改變,幾種異常情況都可能出現。極易出錯,沒有安全性可言,基本不會使用。

  • 讀已提交(Read Committed):該隔離級別指一個事務只能看到其他事務的已經提交的更新,看不到未提交的更新,消除了髒讀和第一類丟失更新,這是大多數資料庫的預設隔離級別,如Oracle,Sqlserver。

  • 可重複讀(Repeatable Read):該隔離級別指一個事務中進行兩次或多次同樣的對於資料內容的查詢,得到的結果是一樣的,但不保證對於資料條數的查詢是一樣的,只要存在讀改行資料就禁止寫,消除了不可重複讀和第二類更新丟失,這是Mysql資料庫的預設隔離級別。

  • 序列化(Serializable):意思是說這個事務執行的時候不允許別的事務併發執行.完全序列化的讀,只要存在讀就禁止寫,但可以同時讀,消除了幻讀。這是事務隔離的最高級別,雖然最安全最省心,但是效率太低,一般不會用。

下面是各種隔離級別對各異常的控制能力:

級別\異常 第一類更新丟失 髒讀 不可重複讀 第二類丟失更新 幻讀
讀未提交 Y Y Y Y Y
讀已提交 N N Y Y Y
可重複讀 N N N N Y
序列化 N N N N N

資料庫鎖分類

一般可以分為兩類,一個是悲觀鎖,一個是樂觀鎖,悲觀鎖一般就是我們通常說的資料庫鎖機制,樂觀鎖一般是指使用者自己實現的一種鎖機制,比如hibernate實現的樂觀鎖甚至程式語言也有樂觀鎖的思想的應用。

悲觀鎖:顧名思義,就是很悲觀,它對於資料被外界修改持保守態度,認為資料隨時會修改,所以整個資料處理中需要將資料加鎖。悲觀鎖一般都是依靠關係資料庫提供的鎖機制,事實上關係資料庫中的行鎖,表鎖不論是讀寫鎖都是悲觀鎖。

悲觀鎖按照使用性質劃分:

  • 共享鎖(Share locks簡記為S鎖):也稱讀鎖,事務A對物件T加s鎖,其他事務也只能對T加S,多個事務可以同時讀,但不能有寫操作,直到A釋放S鎖。

  • 排它鎖(Exclusivelocks簡記為X鎖):也稱寫鎖,事務A對物件T加X鎖以後,其他事務不能對T加任何鎖,只有事務A可以讀寫物件T直到A釋放X鎖。

  • 更新鎖(簡記為U鎖):用來預定要對此物件施加X鎖,它允許其他事務讀,但不允許再施加U鎖或X鎖;當被讀取的物件將要被更新時,則升級為X鎖,主要是用來防止死鎖的。因為使用共享鎖時,修改資料的操作分為兩步,首先獲得一個共享鎖,讀取資料,然後將共享鎖升級為排它鎖,然後再執行修改操作。這樣如果同時有兩個或多個事務同時對一個物件申請了共享鎖,在修改資料的時候,這些事務都要將共享鎖升級為排它鎖。這些事務都不會釋放共享鎖而是一直等待對方釋放,這樣就造成了死鎖。如果一個數據在修改前直接申請更新鎖,在資料修改的時候再升級為排它鎖,就可以避免死鎖。

悲觀鎖按照作用範圍劃分:

  • 行鎖:鎖的作用範圍是行級別,資料庫能夠確定那些行需要鎖的情況下使用行鎖,如果不知道會影響哪些行的時候就會使用表鎖。舉個例子,一個使用者表user,有主鍵id和使用者生日birthday當你使用update … where id=?這樣的語句資料庫明確知道會影響哪一行,它就會使用行鎖,當你使用update … where birthday=?這樣的的語句的時候因為事先不知道會影響哪些行就可能會使用表鎖。
  • 表鎖:鎖的作用範圍是整張表。

樂觀鎖:顧名思義,就是很樂觀,每次自己操作資料的時候認為沒有人回來修改它,所以不去加鎖,但是在更新的時候會去判斷在此期間資料有沒有被修改,需要使用者自己去實現。既然都有資料庫提供的悲觀鎖可以方便使用為什麼要使用樂觀鎖呢?對於讀操作遠多於寫操作的時候,大多數都是讀取,這時候一個更新操作加鎖會阻塞所有讀取,降低了吞吐量。最後還要釋放鎖,鎖是需要一些開銷的,我們只要想辦法解決極少量的更新操作的同步問題。換句話說,如果是讀寫比例差距不是非常大或者你的系統沒有響應不及時,吞吐量瓶頸問題,那就不要去使用樂觀鎖,它增加了複雜度,也帶來了額外的風險。

樂觀鎖實現方式:

樂觀鎖不是資料庫自帶的,需要我們自己去實現。樂觀鎖是指操作資料庫時(更新操作),想法很樂觀,認為這次的操作不會導致衝突,在操作資料時,並不進行任何其他的特殊處理(也就是不加鎖),而在進行更新後,再去判斷是否有衝突了。

通常實現是這樣的:在表中的資料進行操作時(更新),先給資料表加一個版本(version)欄位,每操作一次,將那條記錄的版本號加1。也就是先查詢出那條記錄,獲取出version欄位,如果要對那條記錄進行操作(更新),則先判斷此刻version的值是否與剛剛查詢出來時的version的值相等,如果相等,則說明這段期間,沒有其他程式對其進行操作,則可以執行更新,將version欄位的值加1;如果更新時發現此刻的version值與剛剛獲取出來的version的值不相等,則說明這段期間已經有其他程式對其進行操作了,則不進行更新操作。

舉例:

下單操作包括3步驟:

1.查詢出商品資訊

select (status,status,version) from t_goods where id=#{id}

2.根據商品資訊生成訂單

3.修改商品status為2

update t_goods 

set status=2,version=version+1

where id=#{id} and version=#{version};

除了自己手動實現樂觀鎖之外,現在網上許多框架已經封裝好了樂觀鎖的實現,如hibernate,需要時,可能自行搜尋"hiberate 樂觀鎖"試試看。

  • 版本號(記為version):就是給資料增加一個版本標識,在資料庫上就是表中增加一個version欄位,每次更新把這個欄位加1,讀取資料的時候把version讀出來,更新的時候比較version,如果還是開始讀取的version就可以更新了,如果現在的version比老的version大,說明有其他事務更新了該資料,並增加了版本號,這時候得到一個無法更新的通知,使用者自行根據這個通知來決定怎麼處理,比如重新開始一遍。這裡的關鍵是判斷version和更新兩個動作需要作為一個原子單元執行,否則在你判斷可以更新以後正式更新之前有別的事務修改了version,這個時候你再去更新就可能會覆蓋前一個事務做的更新,造成第二類丟失更新,所以你可以使用update … where … and version=”old version”這樣的語句,根據返回結果是0還是非0來得到通知,如果是0說明更新沒有成功,因為version被改了,如果返回非0說明更新成功。
  • 時間戳(timestamp):和版本號基本一樣,只是通過時間戳來判斷而已,注意時間戳要使用資料庫伺服器的時間戳不能是業務系統的時間。
  • 待更新欄位:和版本號方式相似,只是不增加額外欄位,直接使用有效資料欄位做版本控制資訊,因為有時候我們可能無法改變舊系統的資料庫表結構。假設有個待更新欄位叫count,先去讀取這個count,更新的時候去比較資料庫中count的值是不是我期望的值(即開始讀的值),如果是就把我修改的count的值更新到該欄位,否則更新失敗。java的基本型別的原子型別物件如AtomicInteger就是這種思想。
  • 所有欄位:和待更新欄位類似,只是使用所有欄位做版本控制資訊,只有所有欄位都沒變化才會執行更新。

    樂觀鎖幾種方式的區別:

    新系統設計可以使用version方式和timestamp方式,需要增加欄位,應用範圍是整條資料,不論那個欄位修改都會更新version,也就是說兩個事務更新同一條記錄的兩個不相關欄位也是互斥的,不能同步進行。舊系統不能修改資料庫表結構的時候使用資料欄位作為版本控制資訊,不需要新增欄位,待更新欄位方式只要其他事務修改的欄位和當前事務修改的欄位沒有重疊就可以同步進行,併發性更高。

mysql事務隔離級別實戰

實踐是檢驗真理的唯一標準,掌握上面的理論之後,我們在資料庫上實戰一番家裡更好地掌握也加深理解,同時有助於解決實際問題。不同資料庫很多實現可能不同,這裡以mysql為例講解各種隔離級別下的情況,測試表為user(id,name,gender,passwd,email)。

隔離級別:read-uncommitted

髒讀測試流程: 
1. A設定隔離級別為read-uncommitted(注意這裡未宣告都是session級別,而非全域性的),開啟事務,查詢id=1的記錄 
2. B設定隔離級別為read-uncommitted,開啟事務,修改id=1的記錄,但不提交 
3. A再次查詢id=1的記錄,和第一次查詢的比較一下 
4. B事務回滾,A事務回滾。

A:

這裡寫圖片描述

B:

這裡寫圖片描述

結論:A讀到了B沒有提交的內容,隔離級別為read-uncommitted的時候出現髒讀。

第一類更新丟失測試流程: 
1. A設定隔離級別為read-uncommitted,開啟事務,查詢id=1的記錄 
2. B設定隔離級別為read-uncommitted,開啟事務,查詢id=1的記錄 
3. A修改id=1的記錄 
4. B修改id=1的記錄 
5. A提交 
6. B回滾 
7. A在查詢一次id=1的記錄,看看自己的修改是否成功

結論:結果不如我所想的,A的更新成功了,為什麼呢?A執行update語句的時候對該條記錄加鎖了,B這時候根本無法修改直至超時,也就是至少在mysql中在read-uncommitted隔離級別下驗證第一類丟失更新,據瞭解有的資料庫好像可以設定不加鎖,如果能夠不加鎖的話則可以實現,也貼一下圖吧。

A:

這裡寫圖片描述

B:

這裡寫圖片描述

不可重複讀測試流程(省略):

結論:流程和測試髒讀一樣,其實在第一次測試髒讀的時候就可以發現會出現不可重複讀,A兩次讀取id=1的資料內容不同。

第二類丟失更新流程: 
1. A開啟事務,查詢order_id=1的記錄 
2. B開啟事務,查詢order_id=1的記錄 
3. A把查出來的count加1後更新 
4. B把查出來的count加1更新 
5. A提交,B也提交

A:

這裡寫圖片描述

B:

這裡寫圖片描述

結論:A的更新丟失,我們希望的結果是3,而實際結果是2,跟java的多執行緒很像對不對,read-uncommitted隔離模式下會出現第二類丟失更新。

幻讀測試流程: 
1. A開啟事務,查詢user表所有資料 
2. B開啟事務,新增一條記錄 
3. A再次查詢user表所有記錄,和第一次作比對 
4. A回滾,B回滾

A:

這裡寫圖片描述

B:

這裡寫圖片描述

結論:A兩次查詢全表資料結果不同,read-uncommitted隔離模式下會出現幻讀。

注:因為後面對這幾種異常情況的測試流程基本和上面一樣,個別有些差別讀者自己注意,另外注意更改隔離級別即可,就能看到對應結果,後面的我只給出進一步能解決的異常測試截圖,結論可以參照前面的對照表。

隔離級別:read-committed

髒讀測試截圖

A:

這裡寫圖片描述 
B:

這裡寫圖片描述

結論:A沒有讀到B沒有提交的內容,隔離級別為read-committed的時候不會出現髒讀。

隔離級別:repeatable-read

不可重複讀測試截圖

A:

這裡寫圖片描述

B:

這裡寫圖片描述

結論:A兩次讀取id=1的資料內容相同,repeatable-read隔離模式下不會出現不可重複讀。

隔離級別:Serializable

幻讀測試截圖

A:

這裡寫圖片描述

B:

這裡寫圖片描述

結論:因為A事務未提交之前,B事務插入操作無法獲得鎖而超時,Serializable隔離模式下不會出現幻讀。

何謂悲觀鎖與樂觀鎖

樂觀鎖對應於生活中樂觀的人總是想著事情往好的方向發展,悲觀鎖對應於生活中悲觀的人總是想著事情往壞的方向發展。這兩種人各有優缺點,不能不以場景而定說一種人好於另外一種人。

悲觀鎖

總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖(共享資源每次只給一個執行緒使用,其它執行緒阻塞,用完後再把資源轉讓給其它執行緒)。傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。Java中synchronized和ReentrantLock等獨佔鎖就是悲觀鎖思想的實現。

樂觀鎖

總是假設最好的情況,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號機制和CAS演算法實現。樂觀鎖適用於多讀的應用型別,這樣可以提高吞吐量,像資料庫提供的類似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變數類就是使用了樂觀鎖的一種實現方式CAS實現的。

兩種鎖的使用場景

從上面對兩種鎖的介紹,我們知道兩種鎖各有優缺點,不可認為一種好於另一種,像樂觀鎖適用於寫比較少的情況下(多讀場景),即衝突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果是多寫的情況,一般會經常產生衝突,這就會導致上層應用會不斷的進行retry,這樣反倒是降低了效能,所以一般多寫的場景下用悲觀鎖就比較合適。

樂觀鎖常見的兩種實現方式

樂觀鎖一般會使用版本號機制或CAS演算法實現。

1. 版本號機制

一般是在資料表中加上一個資料版本號version欄位,表示資料被修改的次數,當資料被修改時,version值會加一。當執行緒A要更新資料值時,在讀取資料的同時也會讀取version值,在提交更新時,若剛才讀取到的version值為當前資料庫中的version值相等時才更新,否則重試更新操作,直到更新成功。

舉一個簡單的例子: 假設資料庫中帳戶資訊表中有一個 version 欄位,當前值為 1 ;而當前帳戶餘額欄位( balance )為 $100 。

  1. 操作員 A 此時將其讀出( version=1 ),並從其帳戶餘額中扣除 $50( $100-$50 )。
  2. 在操作員 A 操作的過程中,操作員B 也讀入此使用者資訊( version=1 ),並從其帳戶餘額中扣除 $20 ( $100-$20 )。
  3. 操作員 A 完成了修改工作,將資料版本號加一( version=2 ),連同帳戶扣除後餘額( balance=$50 ),提交至資料庫更新,此時由於提交資料版本大於資料庫記錄當前版本,資料被更新,資料庫記錄 version 更新為 2 。
  4. 操作員 B 完成了操作,也將版本號加一( version=2 )試圖向資料庫提交資料( balance=$80 ),但此時比對資料庫記錄版本時發現,操作員 B 提交的資料版本號為 2 ,資料庫記錄當前版本也為 2 ,不滿足 “ 提交版本必須大於記錄當前版本才能執行更新 “ 的樂觀鎖策略,因此,操作員 B 的提交被駁回。

這樣,就避免了操作員 B 用基於 version=1 的舊資料修改的結果覆蓋操作員A 的操作結果的可能。

2. CAS演算法

compare and swap(比較與交換),是一種有名的無鎖演算法。無鎖程式設計,即不使用鎖的情況下實現多執行緒之間的變數同步,也就是在沒有執行緒被阻塞的情況下實現變數的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS演算法涉及到三個運算元

  • 需要讀寫的記憶體值 V
  • 進行比較的值 A
  • 擬寫入的新值 B

當且僅當 V 的值等於 A時,CAS通過原子方式用新值B來更新V的值,否則不會執行任何操作(比較和替換是一個原子操作)。一般情況下是一個自旋操作,即不斷的重試

樂觀鎖的缺點

ABA 問題是樂觀鎖一個常見的問題

1 ABA 問題

如果一個變數V初次讀取的時候是A值,並且在準備賦值的時候檢查到它仍然是A值,那我們就能說明它的值沒有被其他執行緒修改過了嗎?很明顯是不能的,因為在這段時間它的值可能被改為其他值,然後又改回A,那CAS操作就會誤認為它從來沒有被修改過。這個問題被稱為CAS操作的 "ABA"問題。

JDK 1.5 以後的 AtomicStampedReference 類就提供了此種能力,其中的 compareAndSet 方法就是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設定為給定的更新值。

2 迴圈時間長開銷大

自旋CAS(也就是不成功就一直迴圈執行直到成功)如果長時間不成功,會給CPU帶來非常大的執行開銷。 如果JVM能支援處理器提供的pause指令那麼效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出迴圈的時候因記憶體順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。

3 只能保證一個共享變數的原子操作

CAS 只對單個共享變數有效,當操作涉及跨多個共享變數時 CAS 無效。但是從 JDK 1.5開始,提供了AtomicReference類來保證引用物件之間的原子性,你可以把多個變數放在一個物件裡來進行 CAS 操作.所以我們可以使用鎖或者利用AtomicReference類把多個共享變數合併成一個共享變數來操作。

CAS與synchronized的使用情景

簡單的來說CAS適用於寫比較少的情況下(多讀場景,衝突一般較少),synchronized適用於寫比較多的情況下(多寫場景,衝突一般較多)

  1. 對於資源競爭較少(執行緒衝突較輕)的情況,使用synchronized同步鎖進行執行緒阻塞和喚醒切換以及使用者態核心態間的切換操作額外浪費消耗cpu資源;而CAS基於硬體實現,不需要進入核心,不需要切換執行緒,操作自旋機率較少,因此可以獲得更高的效能。
  2. 對於資源競爭嚴重(執行緒衝突嚴重)的情況,CAS自旋的概率會比較大,從而浪費更多的CPU資源,效率低於synchronized。

補充: Java併發程式設計這個領域中synchronized關鍵字一直都是元老級的角色,很久之前很多人都會稱它為 “重量級鎖” 。但是,在JavaSE 1.6之後進行了主要包括為了減少獲得鎖和釋放鎖帶來的效能消耗而引入的 偏向鎖 和 輕量級鎖 以及其它各種優化之後變得在某些情況下並不是那麼重了。synchronized的底層實現主要依靠 Lock-Free 的佇列,基本思路是 自旋後阻塞競爭切換後繼續競爭鎖稍微犧牲了公平性,但獲得了高吞吐量。線上程衝突較少的情況下,可以獲得和CAS類似的效能;而執行緒衝突嚴重的情況下,效能遠高於CAS。