java併發(7)鎖
從前的日色變得慢,車,馬,郵件都慢,一生只夠愛一個人,
從前的鎖也好看 鑰匙精美有樣子 你鎖了 人家就懂了。
木心先生寫的這首小詩很有情調,一般來說,你鎖住了自己的家門,其他人就進不去了,本文的標題是鎖,當然,這個“鎖”說的不是鎖住家門的鎖,而是java中的鎖,為了更好的理解java中的鎖,先舉個簡單但不怎麼優雅的栗子:
我們有個共享資源,這個資源是馬桶,這個馬桶可以被所有人使用,但是同一時刻只能被同一個人使用(畢竟要兩個人同時使用一個馬桶還是有些不雅),這個時候該怎麼解決這個問題呢,很簡單,將這個馬桶圍起來並加一道門和一把鎖,這就是我們平常使用的衛生間,加了這個門之後有什麼好處呢?我們來模擬一個場景,100個人同時想使用這個馬桶,他們蜂擁而至到達衛生間門口,這時跑的最快的一個人將衛生間門開啟,並且在裡面將門反鎖,開始使用馬桶,接著剩下的99個人也來到了門口,他們要使用馬桶必須將門開啟,所以他們開始嘗試開啟門,但是門已經從裡面反鎖了,他們無奈打不開,只能等在門口,直到第一個人使用完這個共享馬桶,將門開啟,這時所有等待的人開始搶佔衛生間,一個人搶到之後從裡面將門反鎖,其他人又只有等待,依次類推。
以上例子對應於java中的加鎖解鎖過程,而馬桶就是共享資源,若等待的人排隊等候就是公平鎖,否者就是非公平鎖,鎖的概念非常大,java中有多種鎖的實現,而鎖又是併發程式設計中至關重要的一個概念,所以本文作為一個導讀,從大體上描述一下鎖的概念以及java中的鎖,至於對鎖的具體分析,將在後文慢慢補充。
鎖的概念
先來說說鎖的概念,鎖從大類上分為樂觀鎖 和悲觀鎖 ,j.u.c.a包中的類都是樂觀鎖,其他的ReentrantLock、ReentrantReadWriteLock、synchronized是基於悲觀鎖實現的,而StampedLock的讀鎖既有悲觀鎖實現也有樂觀鎖實現。
從小類上來分,鎖又分為可重入鎖 、自旋鎖 、獨佔鎖(互斥鎖) 、共享鎖 、公平鎖 、非公平鎖 等,而我們常說的讀寫鎖其實是兩把鎖,寫鎖是獨佔鎖,讀鎖是共享鎖,接下來闡述一下每種鎖的含義
樂觀鎖
什麼是樂觀鎖,顧名思義,樂觀鎖樂觀的認為共享資料在大多數情況下不會發生衝突,只有少部分時候會發生衝突,在資料提交更新的時候會對衝突進行檢查,當檢查到衝突的時候,會更新失敗,並且返回錯誤,由使用者決定如何對該次失敗進行補償(通常會無限次失敗重試),所以整個過程不會對共享資料上鎖,稱為無鎖(lock-free)。
無鎖的實現是依賴於底層硬體的,無鎖就是指利用處理器的一些特殊的原子指令來避免傳統的加鎖,而java的樂觀鎖實現就是基於CAS的,CAS全稱是CompareAndSwap,譯為比較替換,CAS是無鎖的一種實現,也是樂觀鎖的一種實現,java的Unsafe類有一系列CAS操作的方法,如compareAndSwapInt(Object var1, long var2, int var4, int var5)方法,該方法是一個native方法,該方法最終會通過JNI藉助C語言來呼叫CPU底層指令cmpxchg,並且通過判斷物理機是否是多核的來決定是否在cmpxchg指令前加上lock(lock cmpxchg),cmpxchg是cpu層面的一條cms指令,該指令保證比較和替換兩個操作是原子性的,關於更多的CAS原理詳解參考這篇文章JAVA CAS原理深度分析 。
在理解了CAS的比較和替換兩個操作是原子性的之後(這一點至關重要)再來看java是如何通過CAS來實現樂觀鎖的(這裡強調一下,樂觀鎖是一種思想,而CAS是樂觀鎖的一種實現),CAS操作包含三個運算元 —— 記憶體位置(V)、預期原值(A)和新值(B),只用當預期原值,與記憶體位置中的值相等時,才會把該記憶體位置的值設定為新值,否則不做任何處理。什麼意思呢,舉個栗子:記憶體中有一個值v = 0, 執行緒A、B同時取得v的值為0,這時執行緒A要將v的值設定為1,執行緒A帶上v的預期原值0和新值1,通過CAS,先比較預期原值0等於此時記憶體中v的值0,所以CAS成功,v的值變成了1;此時執行緒B也想將v的值設定為1,由於執行緒B在之前讀取到的v的值為0,所以執行緒B期望此刻記憶體中v的值沒有被其他執行緒改變,所以執行緒B對v的期望原值是0,執行緒B帶上v的期望原值0和新值1,通過CAS,一比較發現期望原值0不等於此刻v在記憶體中的值1,所以設定新值失敗,CAS失敗,這就是怎個CAS過程。
由於本系列不打算對樂觀鎖做過多的探討,所以關於java中使用樂觀鎖實現的Atomic的類,選一個AtomicInteger類在這裡做個簡單分析:
AtomicInteger的用法如下,我們知道普通int型別的i++是一個非原子性的操作,但是a.incrementAndGet方法是一個原子性的自增操作,其依賴於CAS。
AtomicInteger a = new AtomicInteger(0); a.incrementAndGet();
AtomicInteger 持有一個volatile修飾的int型別的value欄位,該欄位就是用來儲存int值的,除此之外還有一個valueOffset欄位,該欄位記錄例項變數value在物件記憶體中的偏移量,簡單來說,通過物件和valueOffset就能找到該物件中value欄位的位置進而取得value的值,這主要是為了能在c++中獲取到AtomicInteger物件的value值。
public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L; // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; ... }
AtomicInteger.incrementAndGet()原始碼:
public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }
可以看到雖然方法名是incrementAndGet,但實際上返回的是unsafe.getAndAddInt + 1,其實這裡取了個巧,因為Unsafe類沒有addAndGetInt這樣的方法。接下來看看Unsafe.getAndAddInt()方法:
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
可以看到該方法有3個引數,第一個var1是object,我們傳的this,第二個var2是valueOffset,正是傳的上文計算出來的valueOffset,第三個引數是要增加多少,前面提到通過物件和valueOffset就可以拿到該物件的value值,所以var5就是通過getIntVloatile方法拿到了記憶體中的value值,並且保證拿到的是最新值,然後在while中通過compareAndSwapInt函式進行CAS操作,var1、var2可以確定記憶體中的value值,var5是期望的value舊值,var5+var4是要替換的value新值,通過上文講到的CAS操作,如果失敗,將會繼續迴圈重試,直到CAS成功,此時記憶體中value值已經+1,跳出迴圈,返回舊值var5,然後上層函式返回舊值+1,從而完成了這一個自增操作。
CAS還有一個ABA問題,就是執行緒A取得變數a的值為0,此時執行緒B將a設定為1,然後又設定為0,雖然執行緒B對變數a進行了多次修改,但是線上程A執行CAS操作的時候發現變數a的值還是0沒有改變(實際上a由0變成了1,然後又變成了0),就會CAS成功,為了解決這個問題,通常是變數前面加上版本號,版本號每次操作都會增加1,由於版本號只會增加不會減少,所以不會出現ABA問題,A-B-A就變成了1A-2B-3A。
關於樂觀鎖的介紹就寫到這裡,後面不再打算繼續探討樂觀鎖,主要是對幾種悲觀鎖進行詳細剖析。
悲觀鎖
與樂觀鎖相反,悲觀鎖悲觀的認為共享資料在大部分時間下都會發生衝突,所以只能在執行緒訪問共享資料的時候將其鎖住,不讓其他執行緒同時訪問,所有執行緒對共享資料的訪問都變成了線性訪問,所以不會產生任何併發問題,在java中 synchronized、ReentrantLock、ReentrantReadWriteLock都是悲觀鎖的實現,關於這幾個類會在後文詳細分析。
公平鎖/非公平鎖
公平鎖和非公平鎖是指,執行緒獲取鎖阻塞的時候是否需要排隊等候,若阻塞的執行緒按照請求鎖的順序獲得鎖,那麼這把鎖是一把公平鎖,若阻塞的執行緒不按照請求鎖的順序獲得鎖,而是採用搶佔式隨機獲得鎖,那麼這把鎖就是一把非公平鎖。
舉個栗子,執行緒A獲得了鎖,此時執行緒BCD依次來請求這把鎖,但是無奈鎖被A持有了,所以BCD只能等待,這時候又兩種策略,一種是BCD排隊等候,因為B比C先來,C比D先來,所以按照BCD的順序排好序,當A釋放了鎖的時候,排在最前面的B獲得鎖,B釋放之後,C獲得,依次類推,這種方式就是公平鎖;另一種策略是BCD不排隊,等A釋放鎖的時候,BCD去搶這把鎖,誰先搶到誰就持有,這種鎖就是非公平鎖。
優缺點:
公平鎖由於維護了執行緒請求順序,保證了時間上的絕對順序,但是在吞吐量上是遠不如非公平鎖的,非公平鎖容易造成飢餓現象,因為非公平鎖是搶佔式的,所以某個執行緒可能長時間無法搶佔到鎖,而處於長時間阻塞狀態
synchronized是一般非公平鎖,ReentrantLock可通過建構函式ReentrantLock(boolean fair)
來指定是否是公平鎖,預設是非公平鎖,ReentrantReadWriteLock同理
可重入鎖
可重入鎖是指,當執行緒A獲得鎖以後,如果執行緒A在此請求該鎖,它能重新獲得該鎖,synchronized和ReentrantLock都是可重入鎖,仔細想想如果synchronized和ReentrantLock是不可重入鎖,那麼當兩個方法A、B都被synchronized修飾,A方法呼叫B方法的時候就會發生死鎖,因為執行A方法的時候執行緒已經獲取了this物件的鎖,然後呼叫B方法的時候又會去請求一次this物件鎖,如果該鎖不可重入,則會等待這把鎖釋放,但是釋放這把鎖需要A方法執行完成,所以就形成了死鎖
表現在程式碼上:
public class SynchronizedTest { public static void main(String[] args){ new SynchronizedTest().methodA(); } public synchronized void methodA() { System.out.println("A is execute ... "); B(); } public synchronized void methodB() { System.out.println("B is execute ... "); } } //從結果上看,A()B()方法都得到了執行,沒有發生死鎖 == 輸出 == A is execute ... B is execute ...
自旋鎖
自旋鎖是指在獲取鎖失敗之後不進入阻塞,而是通過在迴圈體裡面進行不斷的重試,前面樂觀鎖中提到,Unsafe類的CAS操作,在CAS失敗的時候會在迴圈體裡面不斷重試,直到CAS成功才退出迴圈,這其實也是一種自旋的表現。
自旋鎖由於不會讓執行緒真正的掛起,而是不停的執行迴圈,所以少了執行緒狀態的切換,清空cpu快取及重新載入cpu快取的操作,所以響應速度更快,但是由於自旋鎖會佔用cpu時間片,所以當執行緒數量多了之後效能會明顯下降
在jdk8中對synchronized關鍵字進行了一系列優化,引入了輕量級鎖、重量級鎖、偏向鎖、自適應自旋鎖等手段,其中自適應自旋鎖就是在獲取鎖失敗之後開始自旋,自旋次數不固定,而是根據以前執行緒自旋期間成功獲取到錯的次數,也就是說,如果上一個執行緒通過自旋獲取到了鎖,那麼認為這一個執行緒通過自旋獲取鎖的成功率會很高,所以當前執行緒的自旋次數會增加,相反,如果上一個執行緒達到最大自旋次數任然沒有獲取到鎖,那麼認為自旋獲取鎖的失敗率會很高,所以當前執行緒的自旋次數會減少,甚至可能出現若是多個執行緒連續自旋獲取鎖失敗,那麼當前執行緒不再自旋,而是直接掛起。加入自適應自旋鎖很好的解決了:如果自旋次數固定,由於任務的差異,導致每次的最佳自旋次數有差異,不好拿捏自旋次數的問題,而是通過“智慧學習”的方式動態改變自旋次數
關於synchronized的優化手段還有很多,由於篇幅限制,將在後文單獨寫一篇來闡述。
獨佔鎖/共享鎖
獨佔鎖和共享鎖也是一個比較大的概念,如果一把鎖同一時刻只能被一個執行緒持有,則這把鎖是獨佔鎖,如果一把鎖同一時刻能被多個執行緒同時持有,則這把鎖叫做共享鎖。
java中的synchronized和ReentrantLock都是獨佔鎖,而ReentrantReadWriteLock的寫鎖是一把獨佔鎖,讀鎖是一把共享鎖,關於讀寫鎖ReentrantReadWriteLock和可重入鎖ReentrantLock將在後文詳細介紹。
後記
本文簡單的闡述了幾種鎖的概念,以及java類中用到的幾種鎖,簡單總結一下:
- atomic: 樂觀鎖、共享鎖、無鎖、非阻塞同步
- synchronized:悲觀鎖、獨佔鎖、可重入鎖、非公平鎖、自旋鎖
- ReentrantLock: 悲觀鎖、可重入鎖、(公平鎖|非公平鎖)、獨佔鎖
- ReentrantReadWriteLock.ReadLock: 悲觀鎖、可重入鎖、(公平鎖|非公平鎖)、共享鎖
- ReentrantReadWriteLock.WriteLock: 悲觀鎖、可重入鎖、(公平鎖|非公平鎖)、獨佔鎖
關於幾種鎖的基本概念暫時先了解到這裡,現在有個顯而易見的問題,我們說了這麼久的鎖,這把鎖到底存放在什麼地方,這把鎖到低鎖住的是什麼,我們經常使用的同步程式碼塊語法:synchronized(this){}
又是什麼意思?這一系列問題以及上文提到的synchronized鎖優化內容都將會在下一篇文章中詳細介紹。
可以卑微如塵土,不可扭曲如蛆蟲