聊聊Synchronized的優(yōu)化
早期版本synchronized性能較低的原因
在早期版本中,synchronized是一種重量級鎖,其底層由Monitor實(shí)現(xiàn),而Monitor又依賴于操作系統(tǒng)的Mutex Lock。線程獲取到鎖后,需要切換狀態(tài),而操作系統(tǒng)在實(shí)現(xiàn)線程的切換時,需要從用戶態(tài)轉(zhuǎn)為核心態(tài),這是一個非常耗時,非常重的操作。因此在之前,synchronized是一種重量級鎖。
JDK1.6之后對synchronized的優(yōu)化
現(xiàn)在的synchronized已經(jīng)沒有之前那么笨重了,在虛擬機(jī)層面,對synchronized做了較大的優(yōu)化,引入了自旋鎖、適應(yīng)性自旋鎖、鎖消除、鎖粗化,可以減少鎖操作的開銷。
自旋鎖
有時候,獲取到鎖的線程執(zhí)行的操作耗時極短,為了這么點(diǎn)微不足道的時間,將接下來等待鎖的線程掛起非常的不值得。掛起線程的操作需要在核心態(tài)完成,從用戶態(tài)切換到核心態(tài),耗時比較嚴(yán)重。
因此現(xiàn)在增加這么一樣操作,讓等待鎖的線程執(zhí)行忙循環(huán)等待,不停地去嘗試獲取鎖,像一種自旋的操作,故稱之為自旋鎖。
如果之前線程占有鎖的時間極短,那么自旋鎖的性能將非常的好。但若是占有鎖的時間較長,那么自旋鎖將白白消耗CPU的資源,在自旋次數(shù)到了之后,將會被掛起。
在jdk1.4的時候,自旋鎖默認(rèn)關(guān)閉;jdk1.6之后,自旋鎖默認(rèn)開啟,默認(rèn)自旋10次,當(dāng)然也可以使用PreBlockSpin來修改自旋次數(shù)。
自旋鎖的痛點(diǎn)在于:無法在不同場景中,確定出一個可靠的自旋次數(shù)。因此,衍生出來適應(yīng)性自旋鎖。
適應(yīng)性自旋鎖
在適應(yīng)性自旋鎖中,自旋的次數(shù)不再固定,一般由之前自旋的次數(shù)和鎖持有者的狀態(tài)決定。
如果在一個鎖對象上,之前的線程都能通過自旋來獲取到鎖,并且沒有超過自旋次數(shù),那么虛擬機(jī)認(rèn)為,通過自旋獲取到鎖的概率很大,下一次會增加自旋的次數(shù)。相反的,如果之前很少有線程通過自旋獲取到鎖,那么虛擬機(jī)會減少自旋的次數(shù),減少到一定次數(shù)后,甚至?xí)苯臃艞壸孕?,升級為重量級鎖。
可以看出,適應(yīng)性自旋鎖十分機(jī)智。
鎖消除
從字面意思上可以看出,這是一種直接去除鎖的方法,簡單粗暴。
對于那些根本不可能存在鎖競爭卻又包含鎖的情況,虛擬機(jī)會直接消除這個鎖,避免無意義的鎖請求。比如我在純單線程中對某個方法或者變量加鎖,或者調(diào)用內(nèi)部實(shí)現(xiàn)有鎖的對象(Vector、StringBuffer與HashTable等),虛擬機(jī)會直接消除毫無意義的加鎖。
鎖粗化
在上一文中【多線程】淺說Synchronized,我們談到了synchronized的應(yīng)用-雙重檢驗(yàn)鎖的優(yōu)化過程,強(qiáng)調(diào)將加鎖的范圍盡量限制得小一些,直到存在鎖競爭的實(shí)際區(qū)域才加鎖,這樣程序運(yùn)行更加高效。
但是,如果存在這樣的一種情況:反復(fù)的對同一個對象執(zhí)行加鎖解鎖的操作,也會導(dǎo)致CPU資源的過度消耗。
鎖粗化,就是將反復(fù)的加鎖操作粗化成一個范圍更大的鎖,這樣加鎖只有一次。
例如,在循環(huán)內(nèi)部,調(diào)用StringBuffer的append操作(關(guān)于StringBuffer,可以參考我的另外一篇文章【JAVA】String、StringBuilder、StringBuffer三者的區(qū)別),每次append都需要加鎖,虛擬機(jī)檢測到這種情況后,首先會對append脫鎖,然后進(jìn)行鎖粗化,將鎖的范圍擴(kuò)大到循環(huán)外部。
鎖的狀態(tài)
鎖的狀態(tài)有以下幾種:
- 無鎖狀態(tài)
- 偏向鎖狀態(tài)
- 輕量級鎖狀態(tài)
- 重量級鎖狀態(tài)
其中,無鎖狀態(tài)對應(yīng)于鎖消除,Monitor對應(yīng)于重量級鎖,也就是1.6之前的synchronized。
偏向鎖
偏向鎖的核心要義就體現(xiàn)在“偏”字上,這個鎖偏向第一個獲取到它的線程。
在大部分情況下,不存在激烈的鎖競爭,總是由同一個線程獲取到該鎖。那么為了減少同一個線程獲取鎖帶來的開銷,就引入了偏向鎖。
如果一個線程不斷的獲取到了鎖,那么該鎖就進(jìn)入偏向鎖狀態(tài)。當(dāng)這個線程再次請求鎖時,無需做任何同步操作,直接獲取到鎖。
當(dāng)然,偏向鎖適用于基本無鎖競爭的情況,當(dāng)鎖競爭激烈時,偏向鎖就失去了作用,會升級為輕量級鎖。
輕量級鎖
在偏向鎖的狀態(tài)下,此時又出現(xiàn)了一個線程,與偏向線程競爭該鎖,此時該鎖會升級為輕量級鎖。
舉個例子,比如創(chuàng)建一個線程1執(zhí)行同步print()方法打印奇數(shù),這時候的鎖狀態(tài)為偏向鎖。此時,再創(chuàng)建一個線程2同樣執(zhí)行同步print()方法打印偶數(shù),偏向鎖就會升級為輕量級鎖。線程1打印某個奇數(shù)時,線程2并沒有被掛起,而是處于一種自旋狀態(tài),這種自旋效率很高??墒牵?dāng)我再創(chuàng)建100個線程時,同樣執(zhí)行同步print()方法,自旋的效率將會變得十分低下,此時輕量級鎖會升級為重量級鎖,即使用Monitor來進(jìn)行同步。
鎖的升級
無鎖、偏向鎖、輕量級鎖與重量級鎖,會隨著鎖競爭的升級而升級。
從一開始的偏向鎖,產(chǎn)生鎖競爭后,升級為輕量級鎖,自旋失敗后,升級為重量級鎖,一般來說,鎖的升級是單向的。