synchronize早已經沒那麼笨重

我發現一些同窗在網絡上有看很多synchronize的文章,可能有些同窗沒深刻了解,只看了部份內容,就急急忙忙認爲不能使用它,很笨重,由於是採用操做系統同步互斥信號量來實現的。關於這類的對於synchronize的 污點,我打算幫它清洗下。

JVM鎖優化

其實jdk1.6對鎖的實現已經引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減小鎖操做的開銷。程序員

鎖主要存在四中狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨着競爭的激烈而逐漸升級。注意鎖能夠升級不可降級,這種策略是爲了提升得到鎖和釋放鎖的效率。網絡

重量級鎖

重量級鎖,是JDK1.6以前,內置鎖的實現方式。簡單來講,重量級鎖就是採用互斥量來控制對互斥資源的訪問。多線程

歷史回顧:在JDK1.6之前的版本,synchronized實現的內置鎖都比較重(這也是諸多同窗們理解的版本)。JVM中monitorentermonitorexit字節碼依賴於底層的操做系統的Mutex Lock來實現的,可是因爲使用Mutex Lock須要將當前線程掛起並從用戶態切換到內核態來執行,這種切換的代價是很是昂貴的。然而在現實中的大部分狀況下,同步方法是運行在單線程環境(無鎖競爭環境)若是每次都調用Mutex Lock那麼將嚴重的影響程序的性能。併發

自旋鎖

線程的阻塞和喚醒須要CPU從用戶態轉爲核心態,頻繁的阻塞和喚醒對CPU來講是一件負擔很重的工做,勢必會給系統的併發性能帶來很大的壓力。同時咱們發如今許多應用上面,對象鎖的鎖狀態只會持續很短一段時間,爲了這一段很短的時間頻繁地阻塞和喚醒線程是很是不值得的。因此引入自旋鎖。app

何謂自旋鎖?工具

所謂自旋鎖,就是讓該線程等待一段時間,不會被當即掛起,看持有鎖的線程是否會很快釋放鎖。怎麼等待呢?執行一段無心義的循環便可(自旋)。性能

自旋等待不能替代阻塞,先不說對處理器數量的要求(多核,貌似如今沒有單核的處理器了),雖然它能夠避免線程切換帶來的開銷,可是它佔用了處理器的時間。若是持有鎖的線程很快就釋放了鎖,那麼自旋的效率就很是好,反之,自旋的線程就會白白消耗掉處理的資源,它不會作任何有意義的工做,典型的佔着茅坑不拉屎,這樣反而會帶來性能上的浪費。因此說,自旋等待的時間(自旋的次數)必需要有一個限度,若是自旋超過了定義的時間仍然沒有獲取到鎖,則應該被掛起。優化

自旋鎖在JDK 1.4.2中引入,默認關閉,可是可使用-XX:+UseSpinning開開啓,在JDK1.6中默認開啓。同時自旋的默認次數爲10次,能夠經過參數-XX:PreBlockSpin來調整;spa

若是經過參數-XX:preBlockSpin來調整自旋鎖的自旋次數,會帶來諸多不便。假如我將參數調整爲10,可是系統不少線程都是等你剛剛退出的時候就釋放了鎖(假如你多自旋一兩次就能夠獲取鎖),你是否是很尷尬。因而JDK1.6引入自適應的自旋鎖,讓虛擬機會變得愈來愈聰明。操作系統

適應自旋鎖

JDK 1.6引入了更加聰明的自旋鎖,即自適應自旋鎖。所謂自適應就意味着自旋的次數再也不是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。它怎麼作呢?線程若是自旋成功了,那麼下次自旋的次數會更加多,由於虛擬機認爲既然上次成功了,那麼這次自旋也頗有可能會再次成功,那麼它就會容許自旋等待持續的次數更多。反之,若是對於某個鎖,不多有自旋可以成功的,那麼在之後要或者這個鎖的時候自旋的次數會減小甚至省略掉自旋過程,以避免浪費處理器資源。

有了自適應自旋鎖,隨着程序運行和性能監控信息的不斷完善,虛擬機對程序鎖的情況預測會愈來愈準確,虛擬機會變得愈來愈聰明。

鎖消除

爲了保證數據的完整性,咱們在進行操做時須要對這部分操做進行同步控制,可是在有些狀況下,JVM檢測到不可能存在共享數據競爭,這是JVM會對這些同步鎖進行鎖消除。鎖消除的依據是逃逸分析的數據支持。

若是不存在競爭,爲何還須要加鎖呢?因此鎖消除能夠節省毫無心義的請求鎖的時間。變量是否逃逸,對於虛擬機來講須要使用數據流分析來肯定,可是對於咱們程序員來講這還不清楚麼?咱們會在明明知道不存在數據競爭的代碼塊前加上同步嗎?可是有時候程序並非咱們所想的那樣?咱們雖然沒有顯示使用鎖,可是咱們在使用一些JDK的內置API時,如StringBuffer、Vector、HashTable等,這個時候會存在隱形的加鎖操做。好比StringBuffer的append()方法,Vector的add()方法:

在運行這段代碼時,JVM能夠明顯檢測到變量vector沒有逃逸出方法vectorTest()以外,因此JVM能夠大膽地將vector內部的加鎖操做消除。

鎖粗化

咱們知道在使用同步鎖的時候,須要讓同步塊的做用範圍儘量小—僅在共享數據的實際做用域中才進行同步,這樣作的目的是爲了使須要同步的操做數量儘量縮小,若是存在鎖競爭,那麼等待鎖的線程也能儘快拿到鎖。

在大多數的狀況下,上述觀點是正確的,LZ也一直堅持着這個觀點。可是若是一系列的連續加鎖解鎖操做,可能會致使沒必要要的性能損耗,因此引入鎖粗話的概念。

鎖粗話概念比較好理解,就是將多個連續的加鎖、解鎖操做鏈接在一塊兒,擴展成一個範圍更大的鎖。如上面實例:vector每次add的時候都須要加鎖操做,JVM檢測到對同一個對象(vector)連續加鎖、解鎖操做,會合並一個更大範圍的加鎖、解鎖操做,即加鎖解鎖操做會移到for循環以外。

偏向鎖

既然採用了內置鎖,只要訪問了同步代碼,都會涉及獲取鎖和釋放鎖的動做。而這種動做都是存在開銷的。不管是重量級鎖去取得互斥信號量,仍是輕量級鎖去compare,都會有開銷。而後不少時候,被內置鎖約束的同步代碼段每每只有一個線程去獲取「鎖」,根本不存在併發訪問。那麼這時候頻繁地加鎖和解鎖就會有額外的開銷。所以偏向鎖也應運而生。

在採用偏向鎖時,若是一個線程第一次來訪問互斥資源,則在對象頭和棧幀的鎖記錄中存儲偏向鎖的線程ID(能夠理解爲獲取「鎖」的動做)。偏向鎖在獲取鎖以後,直到有競爭出現纔會釋放鎖。也就是說,若是長期沒有競爭,偏向鎖是一直持有鎖的。這樣,當線程下次再次進入同步塊的時候不須要進行任何獲取鎖的操做,便可訪問互斥資源。節約了頻繁獲取鎖和釋放鎖的開銷。

輕量級鎖

輕量級鎖,顧名思義,相比重量級鎖,其加鎖和解鎖的開銷會小不少。重量級鎖之因此開銷大,關鍵是其存在線程上下文切換的開銷。而輕量級鎖經過JAVA中CAS的實現方式,避免了這種上下文切換的開銷。當compare失敗的時候(理解成沒有拿到」鎖」),線程不會被掛起;當compare成功的時候,能夠直接對互斥資源進行修改(就好像拿到了「鎖同樣」)。重量級鎖使用互斥信號量實現,若是沒有拿到互斥信號量(理解成沒有拿到「鎖」),線程會被掛起;若是拿到互斥信號量則能夠直接對互斥資源進行訪問。

從以上分析可知,實際上是否拿到「鎖」對於不一樣的鎖實現方式有着不一樣的含義。 重量級鎖基於互斥信號量實現,則認爲拿到互斥信號量即爲拿到鎖。而CAS操做則經過compare是否成功來判斷是否拿到「鎖」。 這裏的「鎖」都不是特指某一具體事物,而是一種「條件」,拿到了「鎖」,即意味着知足了「條件」,能夠對互斥資源進行訪問。固然本質上,不管哪一種實現方式,拿到鎖以後都會去修改Mark Word,來記錄本身確實拿到了鎖;釋放鎖則會清空Mark word中本身的線程ID。

輕量級鎖和重量級鎖的重要區別是: 拿不到「鎖」時,是否有線程調度和上下文切換的開銷。

輕量級鎖加鎖:

線程在執行同步塊以前,JVM會先在當前線程的棧楨中建立用於存儲鎖記錄的空間(Lock 
Record),並將對象頭中的Mark Word複製到鎖記錄中,官方稱爲Displaced Mark 
Word。而後線程嘗試使用CAS將對象頭中的Mark 
Word替換爲指向鎖記錄的指針。若是成功,當前線程得到鎖。若是這個更新操做失敗了,虛擬機首先會檢查
對象的Mark Word是否指向當前線程的棧幀。若是指向,說明當前線程已經擁有了這個對象的鎖,那就能夠直
接進入同步塊繼續執行,不然說明這個鎖對象已經被其餘線程搶佔了。若是有兩條以上的線程爭用同一個鎖
,那輕量級鎖就再也不有效,要膨脹爲重量級鎖,Mark Word中存儲的就是指向重量級(互斥量)的指針
複製代碼

輕量級鎖解鎖:

輕量級解鎖時,會使用原子的CAS操做來將Displaced Mark 
Word替換回到對象頭,若是成功,則整個同步過程就完成了。若是替換失敗,說明有其餘線程嘗試過獲取該
鎖,那麼其餘線程就要在釋放鎖的同時,喚醒被掛起的線程。
複製代碼

關於輕量級鎖的加鎖和解鎖過程簡單來講就是:

  • 嘗試CAS修改mark word:若是這步能直接成功,則代價較小,能夠直接獲取鎖
  • 獲取鎖失敗則採用自旋鎖來獲取鎖(CAS修改嘗試失敗後採起的策略)
  • 自旋鎖嘗試失敗,鎖膨脹,成爲重量級鎖:自旋鎖也嘗試失敗,不得不使用重量級鎖,線程也被阻塞。

總結

因此synchronize並有沒像以前想象的那麼笨重,其實你們能夠在大量的源碼中都能看到它的身影,包括juc包下的工具類等等,總之存在必有合理之處,望你們善用它。(固然前提必須理解它)

相關文章
相關標籤/搜索