在多線程併發編程中synchronized一直是元老級角色,不少人都會稱呼它爲重量級鎖。可是,隨着Java對synchronized進行了各類優化以後,有些狀況下它就並不那麼重了。本文詳細介紹Java中爲了減小得到鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖java
這三種鎖由輕到重排序爲:偏向鎖<輕量級鎖<重量級鎖編程
想要了解Java中的鎖,咱們首先須要瞭解一些基礎知識數組
1、鎖類型安全
鎖從宏觀上分類,分爲悲觀鎖與樂觀鎖。多線程
樂觀鎖併發
樂觀鎖是一種樂觀思想,即認爲讀多寫少,遇到併發寫的可能性低,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,採起在寫時先讀出當前版本號,而後加鎖操做(比較跟上一次的版本號,若是同樣則更新),若是失敗則要重複讀-比較-寫的操做。框架
java中的樂觀鎖基本都是經過CAS操做實現的,CAS是一種更新的原子操做,比較當前值跟傳入值是否同樣,同樣則更新,不然失敗。jvm
悲觀鎖函數
悲觀鎖是就是悲觀思想,即認爲寫多,遇到併發寫的可能性高,每次去拿數據的時候都認爲別人會修改,因此每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會block直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嚐試cas樂觀鎖去獲取鎖,獲取不到,纔會轉換爲悲觀鎖,如RetreenLock。高併發
2、Java 線程的實現以及切換開銷
對於計算機實現線程主要有3種方式:使用內核線程實現(一對一)、使用用戶線程實現(一對N)、使用用戶線程加輕量級進程混合實現(N對M)。對於Sun JDK來講,它的Windows版與Linux版都是使用一對一的線程模型實現的,一條Java線程就映射到一條輕量級進程之中,由於Windows和Linux系統提供的線程模型就是一對一的。然而使用內核線程實現的一對一模型是基於內核線程實現的,因此各類線程操做,如建立、析構及同步,都須要進行系統調用。而系統調用的代價相對較高,須要在用戶態(User Mode)和內核態(Kernel Mode)中來回切換。由於用戶態與內核態都有各自專用的內存空間,專用的寄存器等,用戶態切換至內核態須要傳遞給許多變量、參數給內核,內核也須要保護好用戶態在切換時的一些寄存器值、變量等,以便內核態調用結束後切換回用戶態繼續工做。
所以,頻繁的線程切換將會嚴重拖慢咱們的系統性能,耗費不少CPU處理時間,而且對於簡單的同步代碼塊,獲取鎖和釋放鎖的時間可能比用戶代碼的執行時間還要長,這樣就顯得很是糟糕。
然而synchronized在線程爭用不到鎖的時候將會線程阻塞,而且獲取鎖的時候還須要從阻塞狀態醒來,這就是兩次切換開銷。所以爲了解決這種頻繁的線程切換致使的性能問題,Java引入了偏向鎖和輕量級鎖。
3、Java對象頭Markword
Java對象頭裏的。若是對象是數組類型,則虛擬機用3個字寬
(Word)存儲對象頭,若是對象是非數組類型,則用2字寬存儲對象頭。在32位虛擬機中,1字寬
等於4字節,即32bit,以下表所示。
Java對象頭裏的Mark Word裏默認存儲對象的HashCode、分代年齡和鎖標記位。32位JVM的Mark Word的默認存儲結構以下表所示。
在運行期間,Mark Word裏存儲的數據會隨着鎖標誌位的變化而變化。Mark Word可能變化爲存儲如下4種數據,以下表所示。
在64位虛擬機下,Mark Word是64bit大小的,其存儲結構以下表所示。
markword數據的長度在32位和64位的虛擬機(未開啓壓縮指針)中分別爲32bit和64bit,它的最後2bit是鎖狀態標誌位,用來標記當前對象的狀態,對象的所處的狀態,決定了markword存儲的內容,以下表所示:
狀態 | 標誌位 | 存儲內容 |
---|---|---|
未鎖定 | 01 | 對象哈希碼、對象分代年齡 |
輕量級鎖定 | 00 | 指向鎖記錄的指針 |
膨脹(重量級鎖定) | 10 | 執行重量級鎖定的指針 |
GC標記 | 11 | 空(不須要記錄信息) |
可偏向 | 01 | 偏向線程ID、偏向時間戳、對象分代年齡 |
4、Java中的鎖
Java SE 1.6爲了減小得到鎖和釋放鎖帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」,在Java SE 1.6中,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭狀況逐漸升級。鎖能夠升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是爲了提升得到鎖和釋放鎖的效率,下文會詳細分析。
(1)偏向鎖
HotSpot[1]的做者通過研究發現,大多數狀況下,鎖不只不存在多線程競爭,並且老是由同一線程屢次得到,爲了讓線程得到鎖的代價更低而引入了偏向鎖。當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,之後該線程在進入和退出同步塊時不須要進行CAS操做來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word裏是否存儲着指向當前線程的偏向鎖。若是測試成功,表示線程已經得到了鎖。若是測試失敗,則須要再測試一下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖):若是沒有設置,則使用CAS競爭鎖;若是設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。
偏向鎖的撤銷
偏向鎖使用了一種等到競爭出現才釋放鎖的機制,因此當其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖。偏向鎖的撤銷,須要等待全局安全點(在這個時間點上沒有正在執行的字節碼)。它會首先暫停擁有偏向鎖的線程,而後檢查持有偏向鎖的線程是否活着,若是線程不處於活動狀態,則將對象頭設置成無鎖狀態;若是線程仍然活着,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要麼從新偏向於其餘線程,要麼恢復到無鎖或者標記對象不適合做爲偏向鎖,最後喚醒暫停的線程。下圖中的線程1演示了偏向鎖初始化的流程,線程2演示了偏向鎖撤銷的流程。
偏向鎖的適用場景
始終只有一個線程在執行同步塊,在它沒有執行完釋放鎖以前,沒有其它線程去執行同步塊,在鎖無競爭的狀況下使用,一旦有了競爭就升級爲輕量級鎖,升級爲輕量級鎖的時候須要撤銷偏向鎖,撤銷偏向鎖的時候會致使stop the word操做;
在有鎖的競爭時,偏向鎖會多作不少額外操做,尤爲是撤銷偏向所的時候會致使進入安全點,安全點會致使stw,致使性能降低,這種狀況下應當禁用,高併發的應用會禁用掉偏向鎖。
關閉偏向鎖
偏向鎖在Java 6和Java 7裏是默認啓用的,可是它在應用程序啓動幾秒鐘以後才激活,若有必要可使用JVM參數來關閉延遲:-XX:BiasedLockingStartupDelay=0。若是你肯定應用程序裏全部的鎖一般狀況下處於競爭狀態,能夠經過JVM參數關閉偏向鎖:-XX:-
UseBiasedLocking=false,那麼程序默認會進入輕量級鎖狀態。
(2)輕量級鎖
輕量級鎖原理很是簡單,若是持有鎖的線程能在很短期內釋放鎖資源,那麼那些等待競爭鎖的線程就不須要作內核態和用戶態之間的切換進入阻塞掛起狀態,它們只須要等一等(自旋),等持有鎖的線程釋放鎖後便可當即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。
輕量級鎖加鎖
線程在執行同步塊以前,JVM會先在當前線程的棧楨中建立用於存儲鎖記錄的空間,並將對象頭中的Mark Word複製到鎖記錄中,官方稱爲Displaced Mark Word。而後線程嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。若是成功,當前線程得到鎖,若是失敗,表示其餘線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
輕量級鎖解鎖
輕量級解鎖時,會使用原子的CAS操做將Displaced Mark Word替換回到對象頭,若是成功,則表示沒有競爭發生。若是失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。圖2-2是兩個線程同時爭奪鎖,致使鎖膨脹的流程圖。
由於自旋會消耗CPU,爲了不無用的自旋(好比得到鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於這個狀態下,其餘線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖以後會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。
輕量級鎖的優缺點
輕量級鎖儘量的減小線程的阻塞,這對於鎖的競爭不激烈,且佔用鎖時間很是短的代碼塊來講性能能大幅度的提高,由於自旋的消耗會小於線程阻塞掛起再喚醒的操做的消耗,這些操做會致使線程發生兩次上下文切換!
可是若是鎖的競爭激烈,或者持有鎖的線程須要長時間佔用鎖執行同步塊,這時候就不適合使用輕量級鎖了,由於輕量級鎖在獲取鎖前一直都是佔用cpu作無用功,同時有大量線程在競爭一個鎖,會致使獲取鎖的時間很長,線程自旋的消耗大於線程阻塞掛起操做的消耗,其它須要cup的線程又不能獲取到cpu,形成cpu的浪費。因此這種狀況下咱們要關閉輕量級鎖;
JVM對於自旋週期的選擇,jdk1.5這個限度是必定的寫死的,在1.6引入了適應性輕量級鎖,適應性輕量級鎖意味着自旋的時間不在是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定,基本認爲一個線程上下文切換的時間是最佳的一個時間,同時JVM還針對當前CPU的負荷狀況作了較多的優化
若是平均負載小於CPUs則一直自旋
若是有超過(CPUs/2)個線程正在自旋,則後來線程直接阻塞
若是正在自旋的線程發現Owner發生了變化則延遲自旋時間(自旋計數)或進入阻塞
若是CPU處於節電模式則中止自旋
自旋時間的最壞狀況是CPU的存儲延遲(CPU A存儲了一個數據,到CPU B得知這個數據直接的時間差)
自旋時會適當放棄線程優先級之間的差別
輕量級鎖的開啓
JDK1.6中-XX:+UseSpinning開啓;
-XX:PreBlockSpin=10 爲自旋次數;
JDK1.7後,去掉此參數,由jvm控制;
5、重量級鎖synchronized
在JDK1.5以前都是使用synchronized關鍵字保證同步的,synchronized的做用相信你們都已經很是熟悉了;
它能夠把任意一個非NULL的對象看成鎖。
synchronized實現邏輯以下圖:
它有多個隊列,當多個線程一塊兒訪問某個對象監視器的時候,對象監視器會將這些線程存儲在不一樣的容器中。
Contention List:競爭隊列,全部請求鎖的線程首先被放在這個競爭隊列中;
Entry List:Contention List中那些有資格成爲候選資源的線程被移動到Entry List中;
Wait Set:哪些調用wait方法被阻塞的線程被放置在這裏;
OnDeck:任意時刻,最多隻有一個線程正在競爭鎖資源,該線程被成爲OnDeck;
Owner:當前已經獲取到所資源的線程被稱爲Owner;
!Owner:當前釋放鎖的線程。
JVM每次從隊列的尾部取出一個數據用於鎖競爭候選者(OnDeck),可是併發狀況下,ContentionList會被大量的併發線程進行CAS訪問,爲了下降對尾部元素的競爭,JVM會將一部分線程移動到EntryList中做爲候選競爭線程。Owner線程會在unlock時,將ContentionList中的部分線程遷移到EntryList中,並指定EntryList中的某個線程爲OnDeck線程(通常是最早進去的那個線程)。Owner線程並不直接把鎖傳遞給OnDeck線程,而是把鎖競爭的權利交給OnDeck,OnDeck須要從新競爭鎖。這樣雖然犧牲了一些公平性,可是能極大的提高系統的吞吐量,在JVM中,也把這種選擇行爲稱之爲「競爭切換」。
OnDeck線程獲取到鎖資源後會變爲Owner線程,而沒有獲得鎖資源的仍然停留在EntryList中。若是Owner線程被wait方法阻塞,則轉移到WaitSet隊列中,直到某個時刻經過notify或者notifyAll喚醒,會從新進去EntryList中。
處於ContentionList、EntryList、WaitSet中的線程都處於阻塞狀態,該阻塞是由操做系統來完成的(Linux內核下采用pthread_mutex_lock內核函數實現的)。
Synchronized是非公平鎖。 Synchronized在線程進入ContentionList時,等待的線程會先嚐試自旋獲取鎖,若是獲取不到就進入ContentionList,這明顯對於已經進入隊列的線程是不公平的,還有一個不公平的事情就是自旋獲取鎖的線程還可能直接搶佔OnDeck線程的鎖資源。
synchronized與static synchronized 的區別
一個是實例鎖(鎖在某一個實例對象上,若是該類是單例,那麼該鎖也具備全局鎖的概念),一個是全局鎖(該鎖針對的是類,不管實例多少個對象,那麼線程都共享該鎖)。實例鎖對應的就是synchronized關鍵字,而類鎖(全局鎖)對應的就是static synchronized(或者是鎖在該類的class或者classloader對象上)。
synchronized是對類的當前實例(當前對象)進行加鎖,防止其餘線程同時訪問該類的該實例的全部synchronized塊,注意這裏是「類的當前實例」, 類的兩個不一樣實例就沒有這種約束了。
那麼static synchronized剛好就是要控制類的全部實例的併發訪問,static synchronized是限制多線程中該類的全部實例同時訪問jvm中該類所對應的代碼塊。實際上,在類中若是某方法或某代碼塊中有 synchronized,那麼在生成一個該類實例後,該實例也就有一個監視塊,防止線程併發訪問該實例的synchronized保護塊,而static synchronized則是全部該類的全部實例公用得一個監視塊,這就是他們兩個的區別。也就是說synchronized至關於 this.synchronized,而static synchronized至關於Something.synchronized.
那麼,假若有Something類的兩個實例x與y,那麼下列各組方法被多線程同時訪問的狀況是怎樣的?
a. x.isSyncA()與x.isSyncB()
b. x.isSyncA()與y.isSyncA()
c. x.cSyncA()與y.cSyncB()
d. x.isSyncA()與Something.cSyncA()
這裏,很清楚的能夠判斷:
a,都是對同一個實例(x)的synchronized域訪問,所以不能被同時訪問。(多線程中訪問x的不一樣synchronized域不能同時訪問)
若是在多個線程中訪問x.isSyncA(),由於仍然是對同一個實例,且對同一個方法加鎖,因此多個線程中也不能同時訪問。(多線程中訪問x的同一個synchronized域不能同時訪問)
b,是針對不一樣實例的,所以能夠同時被訪問(對象鎖對於不一樣的對象實例沒有鎖的約束)
c,由於是static synchronized,因此不一樣實例之間仍然會被限制,至關於Something.isSyncA()與 Something.isSyncB()了,所以不能被同時訪問。
那麼,第d呢?,書上的 答案是能夠被同時訪問的,答案理由是synchronzied的是實例方法與synchronzied的類方法因爲鎖定(lock)不一樣的緣由。
synchronized方法與synchronized代碼快的區別
synchronized methods(){} 與synchronized(this){}之間沒有什麼區別,只是synchronized methods(){} 便於閱讀理解,而synchronized(this){}能夠更精確的控制衝突限制訪問區域,有時候表現更高效率。
6、鎖的優缺點對比
下表是鎖的優缺點的對比。
參考:
https://blog.csdn.net/zqz_zqz/article/details/70233767
《Java併發編程的藝術》
《深刻理解Java虛擬機 JVM高級特性與最佳實戰》