一,基本概念html
注意:樂觀鎖本質沒有鎖,所以使用它能夠提升代碼執行效率,不會阻塞,不會等待,會重試。java
二,鎖的實例算法
樂觀鎖:1.在Java中java.util.concurrent.atomic包下面的原子變量類(採用CAS機制)。數據庫
2.版本號機制。 編程
悲觀鎖:悲觀鎖的實現方式就是加鎖,經過給代碼塊加鎖,或數據加鎖。數組
1.給數據加鎖--傳統的關係型數據庫中的鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。安全
2.給代碼塊加鎖--好比Java裏面的同步原語synchronized關鍵字的實現也是悲觀鎖。多線程
1,CAS機制併發
CAS操做方式:即compare and swap 或者compare and set ,涉及到三個操做數,數據所在的內存地址(V),預期值(A),新值(B)。當須要更新時,判斷當前內存地址的值與以前取到的值是否相等,若相等,則用新值更新,若不等則重試,通常狀況下是一個自旋操做,即不斷的重試。源碼分析
CAS 操做中包含三個操做數 —— 須要讀寫的內存位置(V)、進行比較的預期原值(A)和擬寫入的新值(B)。若是內存位置V的現值與預期原值A相匹配,那麼處理器會自動將該位置值更新爲新值B,不然處理器將重複匹配。不管哪一種狀況,它都會在 CAS 指令以前返回該位置的值。(在 CAS 的一些特殊狀況下將僅返回 CAS 是否成功,而不提取當前值。)
CAS是樂觀鎖技術,當多個線程同時嘗試使用CAS更新同一個變量時,只有其中一個線程能成功更新變量的值,其它線程均會失敗,但失敗的線程並不會被掛起,只是被告知此次競爭失敗,而且容許失敗的線程再次嘗試,固然也容許失敗的線程放棄操做。這裏再強調一下,樂觀鎖是一種思想,CAS是這種思想的一種實現方式。雖然與加鎖相比,CAS比較交換會使程序看起來更加複雜一些。但因爲其非阻塞性,對死鎖問題天生免疫,更重要的是,使用無鎖的方式徹底沒有鎖競爭帶來的系統開銷,也沒有線程間頻繁調度帶來的開銷,所以,它要比基於鎖方式的實現擁有更優越的性能。
在Java1.5以前是靠synchronized關鍵字保證同步的,這是一種獨佔鎖,也是一種悲觀鎖,是一個重量級的操做,由於加鎖須要消耗額外的資源,還由於線程狀態的切換會涉及操做系統核心態和用戶態的轉換;因此在1.5以後Java增長了併發包Java.util.concurrent.*(j.u.c)。J.U.C就是創建在CAS之上的,相對於對於 synchronized 這種阻塞算法,CAS是非阻塞算法的一種常見實現。因此J.U.C在性能上有了很大的提高。不過隨着JVM對鎖進行的一系列優化(如自旋鎖、輕量級鎖、鎖粗化等),synchronized的性能表現也已經愈來愈好。
如今來介紹AtomicInteger。AtomicInteger是java.util.concurrent.atomic包提供的原子類,利用CPU提供的CAS操做來保證原子性;除了AtomicInteger外,還有AtomicBoolean、AtomicLong、AtomicReference等衆多原子類。原子操做類大體能夠分爲4類:原子更新基本類型,原子更新數組類型,原子更新引用類型,原子更新屬性類型。這些原子類中都是用了無鎖的概念,有的地方直接使用了CAS機制。
下面以 java.util.concurrent 中的 AtomicInteger 爲例,看一下在不使用鎖的狀況下是如何保證線程安全的。主要理解 getAndIncrement 方法,該方法的做用至關於 ++i 操做。
public class AtomicInteger extends Number implements java.io.Serializable { //在沒有鎖的機制下,字段value要藉助volatile原語,保證線程間的數據是可見性。 private volatile int value; //Unsafe用於實現對底層資源的訪問 private static final Unsafe unsafe = Unsafe.getUnsafe(); //valueOffset是value在內存中的偏移量 private static final long valueOffset; //經過Unsafe得到valueOffset static { try { valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } public final int getAndIncrement() {//至關於++i操做 for (;;) { int current = get();//獲取值 int next = current + 1;//+1操做 if (compareAndSet(current, next))//current是預期值,即從主存中取來還未操做過的值,next更新後的值 return current; } } }
源碼分析說明以下:
(1)getAndIncrement()實現的自增操做是自旋CAS操做:每次獲取數據時,都要循環進行compareAndSet判斷,若是執行成功則退出,不然一直執行。
(2)其中compareAndSet是CAS操做的核心,它是利用Unsafe對象實現的。其中unsafe.compareAndSwapInt(this, valueOffset, expect, update);是藉助C來調用CPU底層指令實現的。
1 public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
相似以下邏輯:
1 if (o == expected) {//this=expect 2 o = x //this=updata 3 return true; 4 } else { 5 return false; 6 }
(3)Unsafe是用來幫助Java訪問操做系統底層資源的類(如能夠分配內存、釋放內存),經過Unsafe,Java具備了底層操做能力,能夠提高運行效率;強大的底層資源操做能力也帶來了安全隱患(類的名字Unsafe也在提醒咱們這一點),所以正常狀況下用戶沒法使用。AtomicInteger在這裏使用了Unsafe提供的CAS功能。
(4)valueOffset能夠理解爲value在內存中的偏移量,對應了CAS三個操做數(V/A/B)中的V;偏移量的得到也是經過Unsafe實現的。
(5)value域的volatile修飾符:Java併發編程要保證線程安全,須要保證原子性、可視性和有序性;CAS操做能夠保證原子性,而volatile能夠保證可視性和必定程度的有序性;在AtomicInteger中,volatile和CAS一塊兒保證了線程安全性。關於volatile做用原理的說明涉及到Java內存模型(JMM),這裏不詳細展開。
2.版本號機制
通常是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一,當線程A要更新數據時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值與當前主內存中的version值相同,則提交更新,若不相同,則重複更新,直到更新成功。
eg:updata table set x=x+1,version=version+1 where id=#{id} and version=#{version};
對於這句代碼來講,若是可以使用id+version查到數據,說明該數據沒有再被修改過。若是查詢不到,則說明數據被修改過,則會重複查詢。
須要注意的是,這裏使用了版本號做爲判斷數據變化的標記,實際上能夠根據實際狀況選用其餘可以標記數據版本的字段,如時間戳等。
悲觀鎖機制存在如下問題:
1. 在多線程競爭下,加鎖、釋放鎖會致使比較多的上下文切換和調度延時,引發性能問題。
2. 一個線程持有鎖會致使其它全部須要此鎖的線程掛起。
3. 若是一個優先級高的線程等待一個優先級低的線程釋放鎖會致使優先級倒置,引發性能風險。
對比於悲觀鎖的這些問題,另外一個更加有效的鎖就是樂觀鎖。其實樂觀鎖就是:每次不加鎖而是假設沒有併發衝突而去完成某項操做,若是由於併發衝突失敗就重試,直到成功爲止。
要注意樂觀鎖的實現本質是沒有使用鎖的:
(1)樂觀鎖自己是不加鎖的,只是在更新時判斷一下數據是否被其餘線程更新了;AtomicInteger即是一個例子。
(2)有時樂觀鎖可能與加鎖操做合做,例如,在前述updateCoins()的例子中,MySQL在執行update時會加排它鎖。但這只是樂觀鎖與加鎖操做合做的例子,不能改變「樂觀鎖自己不加鎖」這一事實。
三,樂觀鎖和悲觀鎖的使用場景
與悲觀鎖相比,樂觀鎖適用的場景受到了更多的限制,不管是CAS仍是版本號機制。
例如,CAS只能保證單個變量操做的原子性,當涉及到多個變量時,CAS是無能爲力的,而synchronized則能夠經過對整個代碼塊加鎖來處理。再好比版本號機制,若是query的時候是針對表1,而update的時候是針對表2,也很難經過簡單的版本號來實現樂觀鎖。
若是悲觀鎖和樂觀鎖均可以使用,那麼選擇就要考慮競爭的激烈程度:
四,CAS機制的缺點
假設有兩個線程——線程1和線程2,兩個線程按照順序進行如下操做:
(1)線程1讀取內存中數據爲A;
(2)線程2將該數據修改成B;
(3)線程2將該數據修改成A;
(4)線程1對數據進行CAS操做
在第(4)步中,因爲內存中數據仍然爲A,所以CAS操做成功,但實際上該數據已經被線程2修改過了。這就是ABA問題。
在AtomicInteger的例子中,ABA彷佛沒有什麼危害。可是在某些場景下,ABA卻會帶來隱患,例如棧頂問題:一個棧的棧頂通過兩次(或屢次)變化又恢復了原值,可是棧可能已發生了變化。
對於ABA問題,比較有效的方案是引入版本號,內存中的值每發生一次變化,版本號都+1;在進行CAS操做時,不只比較內存中的值,也會比較版本號,只有當兩者都沒有變化時,CAS才能執行成功。從Java1.5開始JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法做用是首先檢查當前引用是否等於預期引用,而且當前標誌是否等於預期標誌,若是所有相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。
在併發衝突機率大的高競爭環境下,若是CAS一直失敗,會一直重試,CPU開銷較大。針對這個問題的一個思路是引入退出機制,如重試次數超過必定閾值後失敗退出。固然,更重要的是避免在高競爭環境下使用樂觀鎖。
CAS的功能是比較受限的,例如CAS只能保證單個變量(或者說單個內存值)操做的原子性,這意味着當涉及到多個變量(內存值)時,CAS也無能爲力。
除此以外,CAS的實現須要硬件層面處理器的支持,在Java中普通用戶沒法直接使用,只能藉助atomic包下的原子類使用,靈活性受到限制。