轉載:https://www.jianshu.com/p/2eb5ad8da4dc
Java中的鎖
常見的鎖有synchronized、volatile、偏向鎖、輕量級鎖、重量級鎖
1、synchronized
-
synchronized是並發編程中接觸的最基本的同步工具,是一種重量級鎖,也是java內置的同步機制,首先我們知道synchronized提供了互斥性的語義和可見性,那么我們可以通過使用它來保證並發的安全。
-
synchronized三種用法:
-
對象鎖
- 當使用synchronized修飾類普通方法時,那么當前加鎖的級別就是實例對象,當多個線程並發訪問該對象的同步方法、同步代碼塊時,會進行同步。
-
類鎖
- 當使用synchronized修飾類靜態方法時,那么當前加鎖的級別就是類,當多個線程並發訪問該類(所有實例對象)的同步方法以及同步代碼塊時,會進行同步。
-
同步代碼塊
-
當使用synchronized修飾代碼塊時,那么當前加鎖的級別就是synchronized(X)中配置的x對象實例,當多個線程並發訪問該對象的同步方法、同步代碼塊以及當前的代碼塊時,會進行同步。
-
使用同步代碼塊時要注意的是不要使用String類型對象,因為String常量池的存在,所以很容易導致出問題。
-
-
synchronized實現原理
- synchronized與其他鎖不同,它是內置在JVM中的,從JVM規范中看,JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步,但兩者的實現細節不一樣。代碼塊同步是使用monitorenter和monitorexit指令實現的,而方法同步是使用另外一種方式實現的。monitorenter指令是在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個monitor與之關聯,當且一個monitor被持有后,它將處於鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲得對象的鎖。
- 方法級的同步是隱式的, 即無須通過字節碼指令來控制, 它實現在方法調用和返回操作之中。 虛擬機可以從方法常量池的方法表結構中的ACC_SYNCHRONIZED訪問標志得知一個方法是否聲明為同步方法。 當方法調用時, 調用指令將會檢查方法的ACC_SYNCHRONIZED訪問標志是否被設置, 如果設置了, 執行線程就要求先成功持有管程, 然后才能執行方法, 最后當方法完成(無論是正常完成還是非正常完成) 時釋放管程。 在方法執行期間, 執行線程持有了管程, 其他任何線程都無法再獲取到同一個管程。 如果一個同步方法執行期間拋出了異常, 並且在方法內部無法處理此異常, 那么這個同步方法所持有的管程將在異常拋到同步方法之外時自動釋放。
-
-
實踐
imagetest()的字節碼標注如下
image
2、volatile
-
可見性
- 我們知道volatile可以看做是一種synchronized的輕量級鎖,他能夠保證並發時,被它修飾的共享變量的可見性,那么他是如何實現可見性的呢?
- 我們從jmm的角度來看一下,每個線程擁有自己的工作內存,實際上線程所修改的共享變量是從主內存中拷貝的副本,當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。
- 我們知道volatile可以看做是一種synchronized的輕量級鎖,他能夠保證並發時,被它修飾的共享變量的可見性,那么他是如何實現可見性的呢?
-
實現原理
- 被volatile修飾的共享變量在進行寫操作的時候:
- 1、將當前處理器緩存行的數據寫回到系統內存。
- 2、這個寫回內存的操作會使在其他CPU里緩存了該內存地址的數據無效。
為了提高處理速度,處理器不直接和內存進行通信,而是先將系統內存的數據讀到內部緩存(L1,L2或其他)后再進行操作,但操作完不知道何時會寫到內存。如果對聲明了volatile的變量進行寫操作,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。但是,就算寫回到內存,如果其他處理器緩存的值還是舊的,再執行計算操作就會有問題。所以,在多處理器下,為了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操作的時候,會重新從系統內存中把數據讀到處理器緩存里
- 被volatile修飾的共享變量在進行寫操作的時候:
-
使用場景
-
1.訪問變量不需要加鎖(加鎖的話使用volatile就沒必要了)
-
2、對變量的寫操作不依賴於當前值(因為他不能保證原子性)
-
3.該變量沒有包含在具有其他變量的不變式中。
-
綜上所述:一般我們會用來修飾狀態標志;讀寫鎖(讀>>寫,對寫加鎖,讀不加鎖);DCL的單例模式中;volatile bean(例如放入HTTPSession中的對象)
了解完上面的知識,我們來做一下對比:
- **相同點**:都保證了可見性
- **不同點** : volatile不能保證原子性,但是synchronized會發生阻塞(在線程狀態轉換中詳說),開銷更大。
3、問題的引入
-
實際上我們知道鎖的本質就是線程等待,可以分為線程阻塞和線程自旋,關於他們的區別:
- 阻塞:要阻塞或喚醒一個線程就需要操作系統介入,需要在戶態與核心態之間切換,這種切換會消耗大量的系統資源。 如果線程狀態切換是一個高頻操作時,這將會消耗很多CPU處理時間, 如果對於那些需要同步的簡單的代碼塊,獲取鎖掛起操作消耗的時間比用戶代碼執行的時間還要長,這種同步策略顯然非常糟糕的。
- 自旋:如果持有鎖的線程能在很短時間內釋放鎖資源,那么那些等待競爭鎖的線程就不需要做內核態和用戶態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),等持有鎖的線程釋放鎖后即可立即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。(線程還是Runnable的,只是在執行空代碼。當然一直自旋也會白白消耗計算資源。)
從java鎖的類型來說,阻塞對應的就是悲觀鎖,自旋對應的就是樂觀鎖。在java中樂觀鎖主要的實現方式就是CAS操作,我們來簡單說一下CAS。
CAS:一個CAS方法包含三個參數CAS(V,E,N)。V表示要更新的變量,E表示預期的值,N表示新值。只有當V的值等於E時,才會將V的值修改為N。如果V的值不等於E,說明已經被其他線程修改了,當前線程可以放棄此操作,也可以再次嘗試次操作直至修改成功。基於這樣的算法,CAS操作即使沒有鎖,也可以發現其他線程對當前線程的干擾(臨界區值的修改),並進行恰當的處理。
-
額外引申技術點:volatile
- 上面說到當前線程可以發現其他線程對臨界區數據的修改,這點可以使用volatile進行保證。volatile實現了JMM中的可見性。使得對臨界區資源的修改可以馬上被其他線程看到。
-
synchronized用的鎖是存在Java對象頭里的。如果對象是數組類型,則虛擬機用3個字寬
(Word)存儲對象頭(也就是24個字節),如果對象是非數組類型,則用2字寬存儲對象頭(16個字節)。在64位虛擬機中,1字寬等於8字節,即64bit。markword數據的長度在32位和64位的虛擬機(未開啟壓縮指針)中分別為32bit和64bit,它的最后2bit是鎖狀態標志位,用來標記當前對象的狀態,對象的所處的狀態,決定了markword存儲的內容(它會根據對象的狀態復用自己的存儲空間)
64位虛擬機在不同狀態下markword結構如下圖所示:JVM源碼中是這么寫的
最后2bit是鎖狀態標志位,用來標記當前對象的狀態,對象的所處的狀態,決定了markword存儲的內容,如下表所示:
3、自旋鎖
-
自旋鎖原理非常簡單,如果持有鎖的線程能在很短時間內釋放鎖資源,那么那些等待競爭鎖的線程就不需要做內核態和用戶態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),等持有鎖的線程釋放鎖后即可立即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。
-
但是線程自旋是需要消耗cup的,說白了就是讓cup在做無用功,線程不能一直占用cup自旋做無用功,所以需要設定一個自旋等待的最大時間。如果持有鎖的線程執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖的線程在最大等待時間內還是獲取不到鎖,這時爭用線程會停止自旋進入阻塞狀態。
-
在JDK 1.6中引入了自適應的自旋鎖。 自適應意味着自旋的時間不再固定了, 而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。 如果在同一個鎖對象上, 自旋等待剛剛成功獲得過鎖, 並且持有鎖的線程正在運行中, 那么虛擬機就會認為這次自旋也很有可能再次成功, 進而它將允許自旋等待持續相對更長的時間, 比如100個循環。 另外, 如果對於某個鎖, 自旋很少成功獲得過, 那在以后要獲取這個鎖時將可能省略掉自旋過程, 以避免浪費處理器資源。
4、偏向鎖
-
偏向鎖(顧名思義,它會偏向於第一個訪問鎖的線程,如果在運行過程中,同步鎖只有一個線程訪問,不存在多線程爭用的情況,則線程是不需要觸發同步的,這種情況下,就會給線程加一個偏向鎖)
-
大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。
-
偏向鎖的獲取
- 1、訪問Mark Word中偏向鎖的標識是否設置成1,鎖標志位是否為01——確認為可偏向狀態。
- 2、如果為可偏向狀態,則測試線程ID是否指向當前線程,如果是,進入步驟(5),否則進入步驟(3)。
- 3、如果線程ID並未指向當前線程,則通過CAS操作競爭鎖。如果競爭成功,則將Mark Word中線程ID設置為當前線程ID,然后執行(5);如果競爭失敗,執行(4)。
- 4、如果CAS獲取偏向鎖失敗,則表示有競爭。當到達全局安全點(safepoint)時獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然后被阻塞在安全點的線程繼續往下執行同步代碼。
- 5、執行同步代碼。
- 以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲着指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖):如果沒有設置,則使用CAS競爭鎖;如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。
-
偏向鎖的撤銷
- 1、當有另外的線程視圖鎖定某個已經被偏向過得對象,jvm就需要撤銷偏向鎖。線程不會主動去釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態,撤銷偏向鎖后恢復到未鎖定(標志位為“01”)或輕量級鎖(標志位為“00”)的狀態。
5、輕量級鎖
- 輕量級鎖是由偏向所升級來的,偏向鎖運行在一個線程進入同步塊的情況下,當第二個線程加入鎖爭用的時候,偏向鎖就會升級為輕量級鎖;
- 在《深入理解java虛擬機中》是這樣說的,如果說輕量級鎖是在無競爭的情況下使用CAS操作去消除同步使用的互斥量, 那偏向鎖就是在無競爭的情況下把整個同步都消除掉, 連CAS操作都不做了。
6、重量級鎖Synchronized
前邊已經介紹了各種鎖,下邊主要介紹它們之間的關系

- 上面幾種鎖都是JVM自己內部實現,當我們執行synchronized同步塊的時候jvm會根據啟用的鎖和當前線程的爭用情況,決定如何執行同步操作;
- 在所有的鎖都啟用的情況下線程進入臨界區時會先去獲取偏向鎖,如果已經存在偏向鎖了,則會嘗試獲取輕量級鎖,如果以上兩種都失敗,則啟用自旋鎖,如果自旋也沒有獲取到鎖,則使用重量級鎖,沒有獲取到鎖的線程阻塞掛起,直到持有鎖的線程執行完同步塊喚醒他們;
- 偏向鎖是在無鎖爭用的情況下使用的,也就是同步開在當前線程沒有執行完之前,沒有其它線程會執行該同步快,一旦有了第二個線程的爭用,偏向鎖就會升級為輕量級鎖,一點有兩個以上線程爭用,就會升級為重量級鎖;如果線程爭用激烈,那么應該禁用偏向鎖。
想要了解更多,環境加入Java架構。
作者:AKyS佐毅
鏈接:https://www.jianshu.com/p/2eb5ad8da4dc
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。