本文是《深刻理解多線程》的第五篇文章,前面幾篇文章中咱們從synchronized的實現原理開始,一直介紹到了Monitor的實現原理。java
經過前面幾篇文章,咱們已經知道:程序員
一、同步方法經過ACC_SYNCHRONIZED
關鍵字隱式的對方法進行加鎖。當線程要執行的方法被標註上ACC_SYNCHRONIZED
時,須要先得到鎖才能執行該方法。《深刻理解多線程(一)——Synchronized的實現原理》安全
二、同步代碼塊經過monitorenter
和monitorexit
執行來進行加鎖。當線程執行到monitorenter
的時候要先得到所鎖,才能執行後面的方法。當線程執行到monitorexit
的時候則要釋放鎖。《深刻理解多線程(四)—— Moniter的實現原理》多線程
三、在HotSpot虛擬機中,使用oop-klass模型來表示對象。每個Java類,在被JVM加載的時候,JVM會給這個類建立一個instanceKlass
,保存在方法區,用來在JVM層表示該Java類。當咱們在Java代碼中,使用new建立一個對象的時候,JVM會建立一個instanceOopDesc
對象,這個對象中包含了對象頭以及實例數據。《深刻理解多線程(二)—— Java的對象模型》併發
四、對象頭中主要包含了GC分代年齡、鎖狀態標記、哈希碼、epoch等信息。對象的狀態一共有五種,分別是無鎖態、輕量級鎖、重量級鎖、GC標記和偏向鎖。《深刻理解多線程(三)—— Java的對象頭》app
在上一篇文章的最後,咱們說過,事實上,只有在JDK1.6以前,synchronized
的實現纔會直接調用ObjectMonitor
的enter
和exit
,這種鎖被稱之爲重量級鎖。工具
高效併發是從JDK 1.5 到 JDK 1.6的一個重要改進,HotSpot虛擬機開發團隊在這個版本中花費了很大的精力去對Java中的鎖進行優化,如適應性自旋、鎖消除、鎖粗化、輕量級鎖和偏向鎖等。這些技術都是爲了在線程之間更高效的共享數據,以及解決競爭問題。oop
本文,主要先來介紹一下自旋、鎖消除以及鎖粗化等技術。性能
這裏簡單說明一下,本文要介紹的這幾個概念,以及後面要介紹的輕量級鎖和偏向鎖,其實對於使用他的開發者來講是屏蔽掉了的,也就是說,做爲一個Java開發,你只須要知道你想在加鎖的時候使用synchronized就能夠了,具體的鎖的優化是虛擬機根據競爭狀況自行決定的。優化
也就是說,在JDK 1.5 之後,咱們即將介紹的這些概念,都被封裝在synchronized中了。
要想把鎖說清楚,一個重要的概念不得不提,那就是線程和線程的狀態。鎖和線程的關係是怎樣的呢,舉個簡單的例子你就明白了。
好比,你今天要去銀行辦業務,你到了銀行以後,要先取一個號,而後你坐在休息區等待叫號,過段時間,廣播叫到你的號碼以後,會告訴你去哪一個櫃檯辦理業務,這時,你拿着你手裏的號碼,去到對應的櫃檯,找相應的櫃員開始辦理業務。當你辦理業務的時候,這個櫃檯和櫃檯後面的櫃員只能爲你本身服務。當你辦完業務離開以後,廣播再喊其餘的顧客前來辦理業務。
這個例子中,每一個顧客是一個線程。 櫃檯前面的那把椅子,就是鎖。 櫃檯後面的櫃員,就是共享資源。 你發現沒法直接辦理業務,要取號等待的過程叫作阻塞。 當你聽到叫你的號碼的時候,你起身去辦業務,這就是喚醒。 當你坐在椅子上開始辦理業務的時候,你就得到鎖。 當你辦完業務離開的時候,你就釋放鎖。
對於線程來講,一共有五種狀態,分別爲:初始狀態(New) 、就緒狀態(Runnable) 、運行狀態(Running) 、阻塞狀態(Blocked) 和死亡狀態(Dead) 。
在前一篇文章中,咱們介紹的synchronized
的實現方式中使用Monitor
進行加鎖,這是一種互斥鎖,爲了表示他對性能的影響咱們稱之爲重量級鎖。
這種互斥鎖在互斥同步上對性能的影響很大,Java的線程是映射到操做系統原生線程之上的,若是要阻塞或喚醒一個線程就須要操做系統的幫忙,這就要從用戶態轉換到內核態,所以狀態轉換須要花費不少的處理器時間。
就像去銀行辦業務的例子,當你來到銀行,發現櫃檯前面都有人的時候,你須要取一個號,而後再去等待區等待,一直等待被叫號。這個過程是比較浪費時間的,那麼有沒有什麼辦法改進呢?
有一種比較好的設計,那就是銀行提供自動取款機,當你去銀行取款的時候,你不須要取號,不須要去休息區等待叫號,你只須要找到一臺取款機,排在其餘人後面等待取款就好了。
之因此能這樣作,是由於取款的這個過程相比較之下是比較節省時間的。若是全部人去銀行都只取款,或者辦理業務的時間都很短的話,那也就能夠不須要取號,不須要去單獨的休息區,不須要聽叫號,也不須要再跑到對應的櫃檯了。
而,在程序中,Java虛擬機的開發工程師們在分析過大量數據後發現:共享數據的鎖定狀態通常只會持續很短的一段時間,爲了這段時間去掛起和恢復線程其實並不值得。
若是物理機上有多個處理器,可讓多個線程同時執行的話。咱們就可讓後面來的線程「稍微等一下」,可是並不放棄處理器的執行時間,看看持有鎖的線程會不會很快釋放鎖。這個「稍微等一下」的過程就是自旋。
自旋鎖在JDK 1.4中已經引入,在JDK 1.6中默認開啓。
不少人在對於自旋鎖的概念不清楚的時候可能會有如下疑問:這麼聽上去,自旋鎖好像和阻塞鎖沒啥區別,反正都是等着嘛。
對於去銀行取錢的你來講,站在取款機面前等待和去休息區等待叫號有一個很大的區別:
那就是若是你在休息區等待,這段時間你什麼都不須要管,隨意作本身的事情,等着被喚醒就好了。
若是你在取款機面前等待,那麼你須要時刻關注本身前面還有沒有人,由於沒人會喚醒你。
很明顯,這種直接去取款機前面排隊取款的效率是比較高。
因此呢,自旋鎖和阻塞鎖最大的區別就是,到底要不要放棄處理器的執行時間。對於阻塞鎖和自旋鎖來講,都是要等待得到共享資源。可是阻塞鎖是放棄了CPU時間,進入了等待區,等待被喚醒。而自旋鎖是一直「自旋」在那裏,時刻的檢查共享資源是否能夠被訪問。
因爲自旋鎖只是將當前線程不停地執行循環體,不進行線程狀態的改變,因此響應速度更快。但當線程數不停增長時,性能降低明顯,由於每一個線程都須要執行,佔用CPU時間。若是線程競爭不激烈,而且保持鎖的時間段。適合使用自旋鎖。
除了自旋鎖以後,JDK中還有一種鎖的優化被稱之爲鎖消除。還拿去銀行取錢的例子說。
你去銀行取錢,全部狀況下都須要取號,而且等待嗎?實際上是不用的,當銀行辦理業務的人很少的時候,可能根本不須要取號,直接走到櫃檯前面辦理業務就行了。
能這麼作的前提是,沒有人和你搶着辦業務。
上面的這種例子,在鎖優化中被稱做「鎖消除」,是JIT編譯器對內部鎖的具體實現所作的一種優化。
在動態編譯同步塊的時候,JIT編譯器能夠藉助一種被稱爲逃逸分析(Escape Analysis)的技術來判斷同步塊所使用的鎖對象是否只可以被一個線程訪問而沒有被髮布到其餘線程。
若是同步塊所使用的鎖對象經過這種分析被證明只可以被一個線程訪問,那麼JIT編譯器在編譯這個同步塊的時候就會取消對這部分代碼的同步。
如如下代碼:
public void f() {
Object hollis = new Object();
synchronized(hollis) {
System.out.println(hollis);
}
}
複製代碼
代碼中對hollis
這個對象進行加鎖,可是hollis
對象的生命週期只在f()
方法中,並不會被其餘線程所訪問到,因此在JIT編譯階段就會被優化掉。優化成:
public void f() {
Object hollis = new Object();
System.out.println(hollis);
}
複製代碼
這裏,可能有讀者會質疑了,代碼是程序員本身寫的,程序員難道沒有能力判斷要不要加鎖嗎?就像以上代碼,徹底不必加鎖,有經驗的開發者一眼就能看的出來的。其實道理是這樣,可是仍是有可能有疏忽,好比咱們常常在代碼中使用
StringBuffer
做爲局部變量,而StringBuffer
中的append
是線程安全的,有synchronized
修飾的,這種狀況開發者可能會忽略。這時候,JIT就能夠幫忙優化,進行鎖消除。
瞭解個人朋友都知道,通常到這個時候,我就會開始反編譯,而後拿出反編譯以後的代碼來證實鎖優化確實存在。
可是,以前不少例子之因此能夠用反編譯工具,是由於那些「優化」,如語法糖等,是在javac編譯
階段發生的,並非在JIT編譯
階段發生的。而鎖優化,是JIT編譯器的功能,因此,沒法使用現有的反編譯工具查看具體的優化結果。(關於javac編譯和JIT編譯的關係和區別,我在個人知識星球中單獨發了一篇文章介紹。)
可是,若是讀者感興趣,仍是能夠看的,只是會複雜一點,首先你要本身build一個fasttest版本的jdk,而後在使用java命令對
.class
文件進行執行的時候加上-XX:+PrintEliminateLocks
參數。並且jdk的模式還必須是server模式。
總之,讀者只須要知道,在使用synchronized
的時候,若是JIT通過逃逸分析以後發現並沒有線程安全問題的話,就會作鎖消除。
不少人都知道,在代碼中,須要加鎖的時候,咱們提倡儘可能減少鎖的粒度,這樣能夠避免沒必要要的阻塞。
這也是不少人緣由是用同步代碼塊來代替同步方法的緣由,由於每每他的粒度會更小一些,這實際上是頗有道理的。
仍是咱們去銀行櫃檯辦業務,最高效的方式是你坐在櫃檯前面的時候,只辦和銀行相關的事情。若是這個時候,你拿出手機,接打幾個電話,問朋友要往哪一個帳戶裏面打錢,這就很浪費時間了。最好的作法確定是提早準備好相關資料,在辦理業務時直接辦理就行了。
加鎖也同樣,把無關的準備工做放到鎖外面,鎖內部只處理和併發相關的內容。這樣有助於提升效率。
那麼,這和鎖粗化有什麼關係呢?能夠說,大部分狀況下,減少鎖的粒度是很正確的作法,只有一種特殊的狀況下,會發生一種叫作鎖粗化的優化。
就像你去銀行辦業務,你爲了減小每次辦理業務的時間,你把要辦的五個業務分紅五次去辦理,這反而拔苗助長了。由於這平白的增長了不少你從新取號、排隊、被喚醒的時間。
若是在一段代碼中連續的對同一個對象反覆加鎖解鎖,實際上是相對耗費資源的,這種狀況能夠適當放寬加鎖的範圍,減小性能消耗。
當JIT發現一系列連續的操做都對同一個對象反覆加鎖和解鎖,甚至加鎖操做出如今循環體中的時候,會將加鎖同步的範圍擴散(粗化)到整個操做序列的外部。
如如下代碼:
for(int i=0;i<100000;i++){
synchronized(this){
do();
}
複製代碼
會被粗化成:
synchronized(this){
for(int i=0;i<100000;i++){
do();
}
複製代碼
這其實和咱們要求的減少鎖粒度並不衝突。減少鎖粒度強調的是不要在銀行櫃檯前作準備工做以及和辦理業務無關的事情。而鎖粗化建議的是,同一我的,要辦理多個業務的時候,能夠在同一個窗口一次性辦完,而不是屢次取號屢次辦理。
自Java 6/Java 7開始,Java虛擬機對內部鎖的實現進行了一些優化。這些優化主要包括鎖消除(Lock Elision)、鎖粗化(Lock Coarsening)、偏向鎖(Biased Locking)以及適應性自旋鎖(Adaptive Locking)。這些優化僅在Java虛擬機server模式下起做用(即運行Java程序時咱們可能須要在命令行中指定Java虛擬機參數「-server」以開啓這些優化)。
本文主要介紹了自旋鎖、鎖粗化和鎖消除的概念。在JIT編譯過程當中,虛擬機會根據狀況使用這三種技術對鎖進行優化,目的是減小鎖的競爭,提高性能。