Synchronized鎖性能優化偏向鎖輕量級鎖升級 多線程中篇(五)


不止一次的提到過,synchronized是Java內置的機制,是JVM層面的,而Lock則是接口,是JDK層面的
盡管最初synchronized的性能效率比較差,但是隨着版本的升級,synchronized已經變得原來越強大了
這也是為什么官方建議使用synchronized的原因
畢竟,他是一個關鍵字啊,這才是親兒子,Lock,終歸差了一點
簡單看下,synchronized大致都經過了哪些重要的變革

重量級鎖

對於最原始的synchronized關鍵字,鎖被稱之為重量級鎖
因為底層依賴監視器,監視器又依賴操作系統底層的互斥鎖,java的線程是內核映射的
如果獲取不到鎖,那么就必然會發生內核態與用戶態的轉換,成本很高,所以效率比較低
所有的優化,其實都是為了將原來的重量級鎖的“重量”變輕...
在現在的版本中,鎖的狀態總共有四種:
  • 無鎖狀態
  • 偏向鎖
  • 輕量級鎖
  • 重量級鎖
很顯然鎖的“重量”從左到右,依次遞增
無所狀態很好理解,新增加的偏向鎖與輕量級鎖,其實就是盡可能的將重量級鎖往“無鎖”的方向靠攏,盡可能的減少重量
減少重量的思路就是,通過一定的邏輯處理與判斷,如果不需要加鎖,那么我就少加一點鎖
繼續之前先介紹兩個概念,Mark Word和CAS

Mark Word

對於每個對象,都有一個對象頭,這部分存儲了對象的必要信息
對象頭中有一個主要區域被稱之為Mark Word(其實就是那一段地址保存的數據的統稱)
其實可以簡單理解為一個數據結構,里面保存了一些必要的數據信息
為了節省空間,並不是每個字段都有空間,不同的鎖狀態,有不同的字段含義,比如說32位,這幾位做什么,那幾位做什么,了解過class字節碼文件的話應該很容易理解這種思維
與本文相關的有下面這些,暫時可以不去思考如何保存的問題,就只需要知道有這么些字段即可(你簡單理解為一個結構體,每個字段都有空間表示也不影響理解此處的敘述)
  • 鎖標志位(什么類型的鎖),他的標志包括:無鎖、輕量級鎖、重量級鎖、偏向鎖
  • 輕量級鎖時會記錄:指向棧中鎖記錄的指針
  • 重量級鎖時會記錄:指向互斥量(重量級鎖)的指針
  • 偏向鎖時會記錄:線程ID   

CAS

再簡單提一個概念CAS

compareAndSwap,比較並替換,是一種實現並發算法時常用到的技術

CAS需要有3個操作數:內存地址V,舊的預期值A,即將要更新的目標值B

比如你要操作一個變量,他的值為A,你希望將他修改為B,這期間不會進行加鎖,當你在修改的時候,你發現值仍舊是A,然后將它修改為B,如果此時值被其他線程修改了,變成了C,那么將不會進行值B的寫入操作,這就是CAS的核心理論,通過這樣的操作可以實現邏輯上的一種“加鎖”,避免了真正去加鎖  

輕量級鎖  

輕量級鎖本質是借助於CAS操作,對於競爭不激烈的場景下,可以減少重量級鎖的使用
線程需要訪問同步代碼體時,會判斷當前狀態是否是無鎖狀態
如果無鎖,將會嘗試通過CAS操作,復制一份Mark Down 並且將Mark Down中的字段指向該線程棧中鎖記錄的指針
  • 成功說明沒有競爭,那么執行同步代碼體;
  • 失敗說明存在競爭,那么鎖會升級為重量級鎖,Mark Down字段修改為指向重量級鎖指針,並且請求鎖的線程會被阻塞
當持有線程執行結束后,會嘗試借助於CAS操作恢復Mark Down
如果有競爭會升級為重量級鎖,Mark Down字段會被修改,CAS操作會失敗,所以:
  • 如果此次CAS成功,鎖釋放完成;
  • 如果失敗,將會釋放鎖並且喚醒被阻塞的線程
簡要邏輯圖如下
image_5c85d806_42f0  
對於輕量級鎖,核心就是CAS操作,因為一旦出現競爭,Mark Down信息將會被修改,而CAS操作的原理就是新值與舊值的對比后再操作,所以CAS操作的成功與否,可以推斷是否有競爭
有競爭那么就會升級為重量級鎖,其他請求線程會被阻塞,該線程執行結束后會喚醒其他阻塞線程;否則無競爭就會釋放退出
很顯然,如果競爭激烈的場景,很快就會升級為重量級鎖,而關於輕量級鎖所有的一切都是徒勞的
不過幸運的是,實踐表明,大多數場景並不會競爭激烈    

偏向鎖

對於輕量級鎖中,需要對Mark Down字段進行維護,以及復制Mark Down,以及多次CAS操作
但是事實上,不少場景中,也的確經常存在只有一個線程訪問的情況
如果只有一個線程來回訪問,那么輕量級鎖的維護相對來說不也是沒有必要的么,是否還可以進一步優化?偏向鎖就是一種優化方案
偏向鎖的提出就是針對於這種場景:
鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得
所以偏向的概念,就是偏向這個線程,它的核心思想就是:
鎖會偏向第一個獲取它的線程,如果不存在競爭,只有一個線程,則持有偏向鎖的線程永遠不需要同步
如果沒有競爭,可以看到出來,偏向鎖的可以約等於是無鎖的
核心原理:
當一個線程訪問同步塊並獲取鎖時,會記錄存儲鎖偏向的線程ID
后續該線程在進入和退出同步塊時不再需要CAS操作來加鎖和解鎖,只需簡單地判斷一下對象頭的Mark Word里是否存儲着指向當前線程的偏向鎖
一個簡要的邏輯如下圖所示
image_5c85d806_2fb8
ps:
上圖中如果線程ID不是當前線程的話,也會繼續進行CAS操作的,一旦CAS失敗,才會需要升級,如果成功了,還是執行同步代碼塊
對於偏向鎖,核心針對通常只有一個線程執行同步代碼的場景
通過記錄偏向鎖ID,對於同一線程,如果無鎖狀態獲取偏向鎖成功或者是偏向鎖,且為該線程,后續的進出無需額外的同步操作:
但是一旦出現競爭,那么就會進行鎖升級,升級為輕量級鎖
小結
輕量級鎖和偏向鎖都是借助於CAS,如果出現問題,將會進行鎖的升級,升級是不可逆的,也就是說只能從低到高,也就是偏向-->輕量級-->重量級,不能夠降級
偏向鎖是對於輕量級鎖的更進一步優化,當然這是有前提的,那就是“場景”
很顯然,對於偏向鎖和輕量級鎖,如果不是同一線程或者線程競爭激烈,將會迅速的從偏向鎖升級為輕量級鎖,然后迅速變為重量級鎖,而偏向鎖和輕量級鎖帶來的一切開銷,都將是額外的開銷,所以二者的開啟與否要根據業務來,不同版本的JDK開啟狀態有所不同

自旋,適應性自旋

在鎖分類一文中,對自旋的概念進行了簡單介紹
再次說下,所謂自旋,不是獲取不到就阻塞,而是在原地等待一會兒,再次嘗試(當然次數或者時長有限),他是以犧牲CPU為代價來換取內核狀態切換帶來的開銷
適應性自旋是針對於自旋的限制,比如次數(時長)的一種優化,如果本次成功下次多等待幾次,如果經常失敗,可能就不自旋了
借助於適應性自旋,可以在CPU時間片的損耗和內核狀態的切換開銷之間相對的找到一個平衡,進而能夠提高性能
從原來的一旦獲取不到就阻塞、狀態切換,轉變為在有的時候可以借助於較小的CPU浪費避免狀態切換的開銷,所以顯然可以提升性能

鎖消除

鎖消除,就是消除鎖
可是,難道好好地一個synchronized方法,最后JVM會把關鍵字去掉嗎?顯然不是這個意思
他指的是刪除非必要的同步
根據代碼逃逸技術,如果判斷到一段代碼中,堆上的數據不會逃逸出當前線程,那么可以認為這段代碼是線程安全的,不必要加鎖
什么是逃逸,比如A方法,調用B方法,B方法將內部創建的一個局部對象,返回給了A,那么這個B中的變量就屬於逃逸了,就存在被其他線程訪問的可能
簡單說除了你寫代碼之外,Java底層包括從編譯器到JVM還有很多工作人員在忙活,人家通過算法一看,你這根本就沒有必要使用同步,就會在實際執行的時候把你的同步去掉
你可能以為,我自己哪有寫很多synchronized修飾的方法?
但是你仔細想一下,你即使不寫,代碼中JDK提供的方法、別人提供的Jar包中的方法,他們用了多少synchronized?
最終不都會進入到你的方法中么?
所以實際代碼中的synchronized(同步)遠比你想象到的要多得多
所以如果可以消除不必要的同步,豈不是性能會有所提升?

鎖粗化(鎖膨脹)

還是以方法調用為例,假如一個A方法,中有三個對象b,c,d,分別調用他們的方法而且都是同步方法
void A(){
b.function();
c.function();
d.function();
}
每個方法都加鎖解鎖,是不是很煩很費電?
如果碰巧他們用的都是同一把鎖呢?是不是可以嘗試進行合並?
也就是說,虛擬機檢測到有一系列連串的對同一個對象加鎖和解鎖操作,就會將其合並成一次范圍更大的加鎖和解鎖操作
多個加鎖解鎖操作,轉變為一次的加鎖和解鎖
加鎖解鎖必然會消耗性能,如果可以進行合並,顯然可以提高性能

小結

以上為大致的synchronized優化過的點
面對一個蒸蒸日上的努力小青年,而且還有那么多自身具有的優勢(隱式鎖相對於顯式鎖,對開發者來說友好了很多,畢竟如果可以,大家都不喜歡多操心的)
有什么理由一定要放棄他呢?
所以除非場景特殊,或者對程序分析后,業務適合,否則盡可能的選擇synchronized吧
synchronized是重量級的,他可以“包治百病”,盡管性能或許沒有你的期望那么好
另外有些優化比如偏向鎖、輕量級鎖,對場景是有要求的,如果不管三七二十一,也許並不一定會提高,反而更差,所以對於鎖優化參數的開閉也需要參照場景


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM