一:java多線程互斥,和java多線程引入偏向鎖和輕量級鎖的緣由?
--->synchronized是在jvm層面實現同步的一種機制。
jvm規範中能夠看到synchronized在jvm裏實現原理,jvm基於進入和退出Monitor對象來實現方法同步和代碼塊同的。在代碼同步的開始位置織入monitorenter,在結束同步的位置(正常結束和異常結束處)織入monitorexit指令實現。線程執行到monitorenter處,將會獲取鎖對象對應的monitor的全部權,即嘗試得到對象的鎖。(任意對象都有一個monitor與之關聯,當且一個monitor被持有後,他處於鎖定狀態).
在線程運行到該代碼塊的時候,讓程序的運行級別從用戶態切換到內核態,把線程掛起,讓cpu經過操做系統指令,去調度多線程之間,誰執行代碼塊,誰進入阻塞狀態。這樣會頻繁出現程序運行狀態的切換,這樣就會大量消耗資源,程序運行的效率低下。緣由是monitorenter與monitorexit這兩個控制多線程同步的bytecode原語,是jvm依賴操做系統互斥(mutex)來實現的。
爲了優化synchronized機制,從java6開始引入偏向鎖,和輕量級鎖,儘可能讓多線程訪問公共資源的時候,不進行程序運行狀態的切換。輕量級鎖本意是爲了減小多線程進入互斥的概率,並非要替代互斥。它利用了cpu原語Compare-And-Swap(cas,彙編指令CMPXCHG),嘗試進入互斥前,進行補救。
二:爲何要自旋或者自適應自旋?
--->前面咱們討論互斥同步的時候,提到了互斥同步對性能最大的影響是阻塞的實現,掛起線程和恢復線程的操做都須要轉入內核態中完成,這些操做給系統的併發性能 帶來了很大的壓力。同時,虛擬機的開發團隊也注意到在許多應用上,共享數據的鎖定狀態只會持續很短的一段時間,爲了這段時間去掛起和恢復線程並不值得。若是物理機器有一個以上的處理器,能讓兩個或以上的線程同時並行執行,咱們就可讓後面請求鎖的那個線程「稍等一會」,但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖。爲了讓線程等待,咱們只須讓線程執行一個忙循環(自旋),這項技術就是所謂的自旋鎖。
--->自旋鎖在JDK1.4.2中就已經引入,只不過默認是關閉的,可使用-XX:+UseSpinning參數來開啓,在JDK 1.6中就已經改成默認開啓了。自旋等待不能代替阻塞,且先不說對處理器數量的要求,自旋等待自己雖然避免了線程切換的開銷,但它是要佔用處理器時間的, 因此若是鎖被佔用的時間很短,自旋等待的效果就會很是好,反之若是鎖被佔用的時間很長,那麼自旋的線程只會白白消耗處理器資源,而不會作任何有用的工做, 反而會帶來性能的浪費。所以自旋等待的時間必需要有必定的限度,若是自旋超過了限定的次數仍然沒有成功得到鎖,就應當使用傳統的方式去掛起線程了。自旋次 數的默認值是10次,用戶可使用參數-XX:PreBlockSpin來更改。
--->在JDK 1.6中引入了自適應的自旋鎖。自適應意味着自旋的時間再也不固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。若是在同一個鎖對象 上,自旋等待剛剛成功得到過鎖,而且持有鎖的線程正在運行中,那麼虛擬機就會認爲此次自旋也頗有可能再次成功,進而它將容許自旋等待持續相對更長的時間, 好比100個循環。另外一方面,若是對於某個鎖,自旋不多成功得到過,那在之後要獲取這個鎖時將可能省略掉自旋過程,以免浪費處理器資源。有了自適應自 旋,隨着程序運行和性能監控信息的不斷完善,虛擬機對程序鎖的情況預測就會愈來愈準確,虛擬機就會變得愈來愈「聰明」了。
三:鎖削除
--->鎖削除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,可是被檢測到不可能存在共享數據競爭的鎖進行削除。鎖削除的主要斷定依據來源於逃逸分析的數據支持,若是判斷到一段代碼中,在堆上的全部數據都不會逃逸出去被其餘線程訪問到,那就能夠把它們看成棧上數據對待, 認爲它們是線程私有的,同步加鎖天然就無須進行。好比(只是說明概念,但實際狀況並不必定如例子)在線程安全的環境中使用stringBuffer進行字符串拼加。則會在java文件編譯的時候,進行鎖銷除。
四:鎖粗化
原則上,咱們在編寫代碼的時候,老是推薦將同步塊的做用範圍限制得儘可能小——只在共享數據的實際做用域中才進行同步,這樣是爲了使得須要同步的操做數量儘量變小,若是存在鎖競爭,那等待鎖的線程也能儘快地拿到鎖。大部分狀況下,上面的原則都是正確的,可是若是一系列的連續操做都對同一個對象反覆加鎖和解鎖,甚至加鎖操做是出如今循環體中的,那即便沒有線程競爭,頻繁地進行互斥同步操做也會致使沒必要要的性能損耗。若是虛擬機探測到有這樣一串零碎的操做都對同一個對象加鎖,將會把加鎖同步的範圍擴展(鎖粗化)到整個操做序列的外部。
五:偏向鎖,輕量級鎖,重量級鎖對比
鎖 |
優勢 |
缺點 |
適用場景 |
偏向鎖 |
加鎖和解鎖不須要額外的消耗,和執行非同步方法相比僅存在納秒級的差距 |
若是線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 |
適用於只有一個線程訪問同步塊場景 |
輕量級鎖 |
競爭的線程不會阻塞,提升了程序的響應速度 |
若是始終得不到索競爭的線程,使用自旋會消耗CPU |
追求響應速度,同步塊執行速度很是快 |
重量級鎖 |
線程競爭不使用自旋,不會消耗CPU |
線程阻塞,響應時間緩慢 |
追求吞吐量,同步塊執行速度較慢 |
任何一個java對象的頭部(monitor)包括:
長度 |
內容 |
說明 |
32/64bit |
Mark Word |
存儲對象的hashcode或鎖信息 |
32/64bit |
類對象的地址 |
存儲到對象類型數據的指針 |
32/64bit |
Array length |
數組的長度(若是當前對象是數組) |
其中Mark Word存儲內容以下
六:鎖的狀態
--->鎖一共有四種狀態(根據重量級由低到高的次序):無鎖狀態,偏向鎖狀態,輕量級鎖狀態,重量級鎖狀態
--->鎖的等級只能夠升級,不能夠降級。這種鎖升級卻不能降級的策略,目的是爲了提升得到鎖和釋放鎖的效率。
七:偏向鎖
大多數狀況下,鎖不只不存在多線程競爭,並且老是由同一線程屢次得到,爲了讓線程得到鎖的代價更低而引入了偏向鎖。當一個線程訪問同步塊並獲取鎖時,會在對象頭Mark Wod和棧幀鎖記錄裏存儲鎖偏向的線程ID,之後該線程在進入和退出同步塊時不須要進行CAS操做來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word裏是否存儲着指向當前線程的偏向鎖。java
- 若是測試成功,表示線程已經得到了鎖。
- 若是測試失敗,則測試一下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖):若是沒設置,則使用CAS競爭鎖(競爭什麼?);若是設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。
--->a線程得到鎖,會在a線程的的棧幀裏建立lockRecord,在lockRecord裏和鎖對象的MarkWord裏存儲線程a的線程id.之後該線程的進入,就不須要cas操做,只須要判斷是不是當前線程。
--->a線程獲取鎖,不會釋放鎖。直到b線程也要競爭該鎖時,a線程纔會釋放鎖。
--->偏向鎖的釋放,須要等待全局安全點(在這個時間點上沒有正在執行的字節碼),它會首先暫停擁有偏向鎖的線程,而後檢查持有偏向鎖的線程是否還活着,若是線程仍然活着,擁有偏向鎖的棧會被執行。若是線程不處於活動狀態,則將鎖對象的MarkWord設置成無鎖狀態。棧幀中的lockRecord和對象頭的MarkWord要麼從新偏向其餘線程,要麼恢復到無鎖,或者標記對象不適合做爲偏向鎖(鎖升級)。最後喚醒暫停的線程。
--->關閉偏向鎖,經過jvm的參數-XX:UseBiasedLocking=false,則默認會進入輕量級鎖。
偏向鎖升級:一個對象剛開始實例化的時候,沒有任何線程來訪問它的時候。它是可偏向的,意味着,它如今認爲只可能有一個線程來訪問它,因此當第一個線程來訪問它的時候,它會偏向這個線程,此時,對象持有偏向鎖。偏向第一個線程,這個線程在修改對象頭MarkWord成爲偏向鎖的時候使用CAS操做,並將對象頭中的ThreadID改爲本身的ID,以後再次訪問這個對象時,只須要對比ID,不須要再使用CAS在進行操做。一旦有第二個線程訪問這個對象,由於偏向鎖不會主動釋放,因此第二個線程能夠看到對象時偏向狀態,這時代表在這個對象上已經存在競爭了,操做系統檢查原來持有該對象鎖的線程是否依然存活,若是掛了,則能夠將對象變爲無鎖狀態,而後從新偏向新的線程,若是原來的線程依然存活,則立刻執行那個線程的操做棧,檢查該對象的使用狀況,若是仍然須要持有偏向鎖,則偏向鎖升級爲輕量級鎖(偏向鎖就是這個時候升級爲輕量級鎖的)。若是不存在使用了,則能夠將對象回覆成無鎖狀態,而後從新偏向。
八:輕量級鎖
輕量級鎖加鎖:線程在執行同步塊以前,JVM會將
對象頭中的Mark Word複製到
棧幀鎖記錄中,官方稱爲Displaced Mark Word。而後線程嘗試使用CAS將對
象頭中的Mark Word替換爲指向
棧幀鎖記錄的指針。若是成功,當前線程得到鎖,若是失敗,表示其餘線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
輕量級鎖解鎖:輕量級解鎖時,會使用原子的CAS操做將Displaced Mark Word替換回到對象頭,若是成功,則表示沒有競爭發生。若是失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。
--->a線程得到鎖,
棧幀鎖記錄中的lock record指針指向
鎖對象的對象頭mark word.再讓mark word 指向lock record.這就是獲取了鎖。
--->輕量級鎖,b線程在鎖競爭時,發現鎖已經被a線程佔用,則b線程不進入內核態,讓b線程自旋,執行空循環,等待a線程釋放鎖。若是,完成自旋策略仍是發現a線程沒有釋放鎖,或者讓c線程佔用了。則b線程試圖將輕量級鎖升級爲重量級鎖。
輕量級鎖認爲競爭存在,可是競爭的程度很輕,通常兩個線程對於同一個鎖的操做都會錯開,或者說稍微等待一下(自旋),另外一個線程就會釋放鎖。 可是當自旋超過必定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹爲重量級鎖,重量級鎖使除了擁有鎖的線程之外的線程都阻塞,防止CPU空轉。
九:重量級鎖
就是讓爭搶鎖的線程從用戶態轉換成內核態。讓cpu藉助操做系統進行線程協調。數組