理解JVM對synchronized進行的優化


synchronized關鍵字初步理解中可以知道synchronized的作用和實現原理是通過monitor對象的獲取和釋放。這裏來講講我對synchronized的優化的理解,那要理解優化,首先得知道問題在哪,那麼先了解monitor對象是如何實現同步的呢。

一、monitor具體的實現的原理

monitor對象實際上很複雜。存儲了,所有在等待獲取該monitor的線程對象,等等。這裏說明時需要用到線程的兩種狀態,也就是運行態和阻塞態。運行態就是線程當前在運行,阻塞態也就是當前線程處於sleep(休眠)狀態,一般是在等待需要的資源可用,那麼java這裏的同步也是同樣的。如下面的代碼

private static Pen instance;
public static synchronized void draw() {...};

假設現在有一支筆(也就是單例模式),所有的實例對象共用一支筆去畫畫(調用draw方法)。

  • (1)假設現在A線程調用了draw()方法,獲得了這個類的鎖,也就是該類對應的Monitor對象。
  • (2)隨後B線程也調用了draw()方法,但是獲取不到筆(因爲A在用)。

這裏的(2)中,獲取不到筆(也就是獲取不到類鎖)要幹嘛呢。總不能一直嘗試去獲取類鎖吧(一直嘗試會消耗CPU週期,浪費運算資源,學過單片機的應該能很容易理解,中斷方式和while方式實現的sleep有什麼區別)。所以需要進入阻塞態等到A用完了,來通知他恢復到運行態

也就是說B獲取不到B後,要進入阻塞態。線程轉入阻塞態需要調用系統函數SuspendThread()(假設是windows系統)。A執行完之後,釋放該類對應的Monitor的使用權。然後A就調用ResumeThread()喚醒線程B。B被喚醒後,獲取到了A釋放了的Monitor對象,繼續運行,直到完成。

1.系統調用產生的性能損耗

從這裏可以知道,monitor對象實際上很複雜,因爲複雜,就算只有一個線程重複的執行draw,獲取鎖也是會特別消耗性能的。而且線程的狀態的變化是要通過系統函數的調用來操控的。實際上系統調用是非常消耗資源的,對性能影響非常大。[至於爲什麼,後面的補充部分的第二個部分進行了說明]。如果A能快速的用完筆呢,但是,B又一下都沒等,就直接進入阻塞態了。如果B稍微等等,那麼就不需要進入阻塞態,也就不需要進行系統調用,減少了性能損耗,提高了執行效率。所以JVM針對這樣的情況進行了優化,除了我們剛剛說的Monitor這種鎖(重量級鎖)加入了兩種新的鎖:偏向鎖輕量級鎖,還加入了一些其他的優化:自旋鎖削除鎖粗化

2.偏向鎖

因爲monitor對象的複雜,就算只有一個線程重複的執行draw,獲取鎖也是會特別消耗性能的。偏向鎖就是用來優化這個情況的。

  1. 如果是隻有一個線程A一直重複執行的情況。
  • A拿到畫筆之後。通過CAS操作把自己的線程ID寫入到該類頭的一個字段中。獲得了該鎖的使用權
  • A用完了畫筆,隨後通過CAS操作把當時寫入的自己的線程ID清空,釋放了該鎖的使用權
    [至於CAS操作是什麼,看下面補充的第三部分]

這樣的處理,就能優化A能很快的處理完的情況,而不用進行系統調用。

2.如果有B也要獲取偏向鎖呢,那就是這樣的情況了。

  • A拿到畫筆之後。通過CAS操作把自己的線程ID寫入到該類頭的一個字段中。獲得了該鎖的使用權
  • B通過CAS操作試着寫入自己的線程ID,但是發現那個地方不是空的而是A的線程ID。
  • 所以,B通知線程A取消掉偏向鎖。
  • A接受到消息之後,暫停當前的任務(實際上就是虛擬機暫停了A),先將偏向鎖取消,也就是CAS操作,把類頭的那個字段清空。並且把類頭上面,的鎖的標誌位,改爲,我們接下來要說的輕量級鎖

所以這三個鎖,是會進行升級,轉化的,會按照下面的順序升級。
偏向鎖 —>輕量級鎖 —>重量級鎖

3.輕量級鎖

經過了上面的升級後,該類的鎖已經由偏向鎖變成了輕量級鎖

  • A和B線程都把對象的類頭重的相關字段複製到自己的線程棧中。
  • A線程通過CAS操作,把共享對象的類頭重的相關字段的內容修改爲自己新建的記錄空間的地址。
  • 這個時候B就會嘗試去獲取輕量級鎖
  • 然後B獲取不到,會進入自旋狀態也就是會進行多次CAS操作嘗試。下面就會有兩種情況。
  • 如果A線程通過,CAS操作,釋放了鎖,B線程隨後獲取到了,那麼就會正常執行下去。
  • 如果B線程嘗試了多次之後,還沒得到,虛擬機就暫停A,然後會把鎖的標誌位改成重量級鎖,並把線程B的狀態信息寫入到Monitor的阻塞隊列中,線程B進入到阻塞狀態,以及A的信息也寫入monitor,然後恢復A的執行。
  • 這個時候鎖就從輕量級鎖,升級成了重量級鎖。

4. 升級是不可逆的

從上面的情況可以看到,升級的過程,總是要暫停那個正在執行的線程,然後進行鎖升級,升級完成後,恢復執行。那麼如果鎖等級可逆。那麼可能會存在鎖升級過多次出現的情況,(可能對大部分情況來說)對性能消耗大。所以JVM實現的這個鎖升級是不可逆的。也就是,例如不可以從重量級鎖,回到之前的鎖。

5.鎖削除

這個其實很好理解,也就是通過分析,發現並不需要加鎖,也能保證執行正確。也就是隻可能會有一個線程的情況。那麼這個鎖,完全可以去除。

6.鎖粗化

這個也很好理解,比如下面的代碼

{
	draw();
	draw();
	draw();
}

那麼這裏的三個過程都要獲取鎖,如果分開,獲取。那麼可能會使得鎖的獲取和釋放次數增加,爲了減少次數,提高性能。就會把三個鎖合成一把鎖。那麼這個時候,就只有一次的獲取和釋放,性能會有提升。當然,如果每個draw()處理時間很久,這時可能就不太好了。

補充:

  1. 這個存儲着線程對象的列表是用來幹嘛的呢?
    答:在A釋放了類對應的Monitor的使用權後,那這個時候B還在睡覺呢,得叫醒B進入運行態。那A怎麼知道有B在等呢。所以這就是爲什麼Monitor對象中存儲了所有在等待獲取該monitor的線程對象。這樣A就知道要叫醒誰了。

  2. 爲什麼系統調用非常消耗資源,對性能影響非常大?
    答:如果學過逆向或者看過《Linux二進制》這本書的同學應該知道。系統調用的方式,彙編來實現的話,可以看看我的這篇文章:GUN C內聯彙編,就是首先把系統調用需要傳遞的參數和調用的函數對應的系統調用編號寫入對應的寄存器中。然後通過特定的指令發起調用請求。這時,你的應用程序會從用戶態轉入內核態,因爲有些特權指令只有內核態可以調用,特殊內存區域只有內核態可以訪問。比如負責線程調度的內存區域。從用戶態轉入內核態執行,就需要將你的用戶態的寄存器信息保存到內存中,然後內核態運行完了之後,將保存的信息恢復到寄存器中,進入用戶態繼續執行。可想而知,內存和寄存器直接數據的換入換出,是非常消耗資源,對性能影響非常大的。

  3. CAS操作是什麼?
    答:CAS全稱爲:Compare and Swap 比較和替換。這個操作有一個特點就是原子性。這個操作如果用JAVA代碼表示如下。至於具體怎麼實現原子性,下面這個代碼的來源處的文章也有說明。

/**
* 假設這段代碼是原子性的,那麼CAS其實就是這樣一個過程
* 版權聲明:該段代碼爲爲CSDN博主「CringKong」的原創文章,遵循 CC 4.0 BY-SA 版* 權協議,轉載請附上原文出處鏈接及本聲明。
* 原文鏈接:https://blog.csdn.net/cringkong/article/details/80533917
*/
public boolean compareAndSwap(int v,int a,int b) {
	if (v == a) {
		v = b;
		return true;
	}else {
		return false;
	}
}

參考:

  • Java synchronized 詳解 : http://www.sohu.com/a/273749069_505779
  • Java併發–Java中的CAS操作和實現原理 : https://blog.csdn.net/cringkong/article/details/80533917
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章