阿里巴巴規(guī)范:
用鎖能夠?qū)崿F(xiàn)數(shù)據(jù)的安全性,但是會帶來性能下降。
無鎖能夠基于線程并行提升程序性能,但是會帶來安全性下降。
求平衡???
synchronized鎖:由對象頭中的Mark Word根據(jù)鎖標志位的不同而被復(fù)用及鎖升級策略
Synchronized 鎖性能變化 jdk5 以前java5 以前,只有Synchronized,這個是操作系統(tǒng)級別的重量級操作。使用synchronized 時,需要用戶態(tài)和內(nèi)核態(tài)之間的切換
當(dāng)我們在系統(tǒng)中執(zhí)行一個程序時,大部分時間是運行在用戶態(tài)下的,在其需要操作系統(tǒng)幫助完成某些它沒有權(quán)力和能力完成的工作時就會切換到內(nèi)核態(tài)
java的線程是映射到操作系統(tǒng)原生線程之上的,如果要阻塞或喚醒一個線程就需要操作系統(tǒng)介入,需要在用戶態(tài)與核心態(tài)之間切換,這種切換會消耗大量的系統(tǒng)資源,因為用戶態(tài)與內(nèi)核態(tài)都有各自專用的內(nèi)存空間,專用的寄存器等,用戶態(tài)切換至內(nèi)核態(tài)需要傳遞給許多變量、參數(shù)給內(nèi)核,內(nèi)核也需要保護好用戶態(tài)在切換時的一些寄存器值、變量等,以便內(nèi)核態(tài)調(diào)用結(jié)束后切換回用戶態(tài)繼續(xù)工作。
在Java早期版本中,synchronized屬于重量級鎖,效率低下,因為監(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的==Mutex Lock(互斥鎖)==來實現(xiàn)的,掛起線程和恢復(fù)線程都需要轉(zhuǎn)入內(nèi)核態(tài)去完成,阻塞或喚醒一個Java線程需要操作系統(tǒng)切換CPU狀態(tài)來完成,這種狀態(tài)切換需要耗費處理器時間,如果同步代碼塊中內(nèi)容過于簡單,這種切換的時間可能比用戶代碼執(zhí)行的時間還長”,時間成本相對較高,這也是為什么早期的synchronized效率低的原因
復(fù)習(xí):為什么任意一個對象都能成為鎖?Monitor可以理解為一種同步工具,也可理解為一種同步機制,常常被描述為一個Java對象。Java對象是天生的Monitor,每一個Java對象都有成為Monitor的潛質(zhì),因為在Java的設(shè)計中 ,每一個Java對象自打娘胎里出來就帶了一把看不見的鎖,它叫做內(nèi)部鎖或者Monitor鎖。
Monitor是在jvm底層實現(xiàn)的,底層代碼是c++。本質(zhì)是依賴于底層操作系統(tǒng)的Mutex Lock實現(xiàn),操作系統(tǒng)實現(xiàn)線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài)的轉(zhuǎn)換,狀態(tài)轉(zhuǎn)換需要耗費很多的處理器時間成本非常高。所以synchronized是Java語言中的一個重量級操作。
Monitor與java對象以及線程是如何關(guān)聯(lián) ?
synchronized 結(jié)合對象頭:
對象頭中的 MarkWorld 標志了該對象使用了哪種鎖。
jdk6 之后Java 6之后,為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了輕量級鎖和偏向鎖
需要有個逐步升級的過程,別一開始就捅到重量級鎖
synchronized的種類以及鎖升級流程多線程訪問分三種情況:
synchronized用的鎖是存在Java對象頭里的Mark Word中
鎖升級功能主要依賴MarkWord中鎖標志位和釋放偏向鎖標志位
不同種類的鎖,鎖指向不同:
初始狀態(tài),一個對象被實例化后,如果還沒有被任何線程競爭鎖,那么它就是無鎖狀態(tài)。(001)
代碼演示:
使用JOL查看無鎖的MarkWord的分布圖
public class JOLTest {public static void main(String[] args) {Object o = new Object();
System.out.println( ClassLayout.parseInstance(o).toPrintable());
}
}
從后往前看,每個字節(jié)從前往后讀:
通過代碼中,看出hashCode的值為0,這是因為只有在調(diào)用hashCode 方法時,才會在MarkWord中標記出來
輸出二進制和十六進制看看對應(yīng)關(guān)系:
此時 紅色的 31 位就表示 hashCode 值,黃色部分是 十六進制表示。
偏向鎖 是什么單線程競爭,當(dāng)線程A第一次競爭到鎖時,通過修改Mark Word中的偏向ID、偏向模式。如果不存在其他線程競爭,那么持有偏向鎖的線程將永遠不需要進行同步。
作用當(dāng)一段同步代碼一直被同一個線程多次訪問,由于只有一個線程訪問那么該線程在后續(xù)訪問時便會自動獲得鎖。
比如: 同一個老顧客來訪,直接老規(guī)矩行方便
我們拿賣票的例子演示:
//第一步 創(chuàng)建資源類,定義屬性和和操作方法
class LTicket {//票數(shù)量
private int number = 30;
//創(chuàng)建可重入鎖
private final ReentrantLock lock = new ReentrantLock();
//賣票方法
public void sale() {//上鎖
lock.lock();
try {//判斷是否有票
if(number >0) {System.out.println(Thread.currentThread().getName()+" :賣出"+(number--)+" 剩余:"+number);
}
} finally {//解鎖
lock.unlock();
}
}
}
public class Demo07 {//第二步 創(chuàng)建多個線程,調(diào)用資源類的操作方法
//創(chuàng)建三個線程
public static void main(String[] args) {LTicket ticket = new LTicket();
new Thread(()->{for (int i = 0; i< 40; i++) {ticket.sale();
}
},"AA").start();
new Thread(()->{for (int i = 0; i< 40; i++) {ticket.sale();
}
},"BB").start();
new Thread(()->{for (int i = 0; i< 40; i++) {ticket.sale();
}
},"CC").start();
}
}
幾乎所有的票都是 CC 賣出的,其實這段同步代碼一直都被 CC 訪問。這時候就會出現(xiàn) 偏向鎖
AA :賣出30 剩余:29
AA :賣出29 剩余:28
AA :賣出28 剩余:27
CC :賣出27 剩余:26
CC :賣出26 剩余:25
CC :賣出25 剩余:24
CC :賣出24 剩余:23
CC :賣出23 剩余:22
CC :賣出22 剩余:21
CC :賣出21 剩余:20
CC :賣出20 剩余:19
CC :賣出19 剩余:18
CC :賣出18 剩余:17
CC :賣出17 剩余:16
CC :賣出16 剩余:15
CC :賣出15 剩余:14
CC :賣出14 剩余:13
CC :賣出13 剩余:12
CC :賣出12 剩余:11
CC :賣出11 剩余:10
CC :賣出10 剩余:9
CC :賣出9 剩余:8
CC :賣出8 剩余:7
CC :賣出7 剩余:6
CC :賣出6 剩余:5
CC :賣出5 剩余:4
CC :賣出4 剩余:3
CC :賣出3 剩余:2
CC :賣出2 剩余:1
CC :賣出1 剩余:0
小總結(jié)Hotspot 的作者經(jīng)過研究發(fā)現(xiàn),大多數(shù)情況下:
多線程的情況下,鎖不僅不存在多線程競爭,還存在鎖由同一線程多次獲得的情況,
偏向鎖就是在這種情況下出現(xiàn)的,它的出現(xiàn)是為了解決只有在一個線程執(zhí)行同步時提高性能。
備注
偏向鎖會偏向于第一個訪問鎖的線程,如果在接下來的運行過程中,該鎖沒有被其他的線程訪問,則持有偏向鎖的線程將永遠不需要觸發(fā)同步。也即偏向鎖在資源沒有競爭情況下消除了同步語句,懶的連CAS操作都不做了,直接提高程序性能
偏向鎖的持有理論落地
在實際應(yīng)用運行過程中發(fā)現(xiàn),“鎖總是同一個線程持有,很少發(fā)生競爭”,也就是說鎖總是被第一個占用他的線程擁有,這個線程就是鎖的偏向線程。
那么只需要在鎖第一次被擁有的時候,記錄下偏向線程ID。這樣偏向線程就一直持有著鎖(后續(xù)這個線程進入和退出這段加了同步鎖的代碼塊時,不需要再次加鎖和釋放鎖。而是直接會去檢查鎖的MarkWord里面是不是放的自己的線程ID)。
如果相等,表示偏向鎖是偏向于當(dāng)前線程的,就不需要再嘗試獲得鎖了,直到競爭發(fā)生才釋放鎖。以后每次同步,檢查鎖的偏向線程ID與當(dāng)前線程ID是否一致,如果一致直接進入同步。無需每次加鎖解鎖都去CAS更新對象頭。如果自始至終使用鎖的線程只有一個,很明顯偏向鎖幾乎沒有額外開銷,性能極高。
如果不等,表示發(fā)生了競爭,鎖己經(jīng)不是總是偏向于同一個線程了,這個時候會嘗試使用CAS
來替換MarkWord里面的線程ID為新線程的ID,
注意: 偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖,線程是不會主動釋放偏向鎖的。
技術(shù)實現(xiàn)
一個synchronized方法被一個線程搶到了鎖時,那這個方法所在的對象就會在其所在的Mark Word中將偏向鎖狀態(tài)位修改,同時還會占用前54位來存儲縣城指針作為標識。若該線程再次訪問同一個synchronized方法時,該線程只需要去對象頭的Mark Word中去判斷一下是否有偏向鎖指向本身的ID,無需再進入Monitor去競爭對象了
舉例說明:
偏向鎖的操作不用直接捅到操作系統(tǒng),不涉及用戶到內(nèi)核轉(zhuǎn)換,不必要直接升級為最高級,我們以一個account對象的“對象頭”為例:
假如有一個線程執(zhí)行到 synchronized 代碼塊的時候,JVM使用CAS操作把線程指針 ID 記錄到Mark Word當(dāng)中,并修改標偏向鎖標識,表示當(dāng)前線程就獲得偏向鎖(通過CAS修改對象頭里的鎖標志位),字面意思是“偏向于第一個獲得它的線程”的鎖。執(zhí)行完同步代碼塊后,線程并不會主動釋放偏向鎖。
這時線程獲得了鎖,可以執(zhí)行同步代碼塊。當(dāng)該線程第二次到達同步代碼塊時會判斷此時持有鎖的線程是否還是自己(持有鎖的線程ID也在對象頭里),JVM通過account對象的Mark Word判斷:當(dāng)前線程ID還在,說明還持有著這個對象的鎖,就可以繼續(xù)進入臨界區(qū)工作。由于之前沒有釋放鎖,這里也就不需要重新加鎖。 如果自始至終使用鎖的線程只有一個,很明顯偏向鎖幾乎沒有額外開銷,性能極高。
結(jié)論:JVM不用和操作系統(tǒng)協(xié)商設(shè)置Mutex(爭取內(nèi)核),它只需要記錄下線程ID就標示自己獲得了當(dāng)前鎖,不用操作系統(tǒng)接入。
上述就是偏向鎖:在沒有其他線程競爭的時候,一直偏向偏心當(dāng)前線程,當(dāng)前線程可以一直執(zhí)行。
偏向鎖 JVM 參數(shù)說明通過java -XX:+PrintFlagsInitial |grep BiasedLock*
(需要在Linux環(huán)境下使用,用git也可以)該指令可以看出 偏向鎖的相關(guān)參數(shù)。
-XX:BiasedLockingStartupDelay
偏向鎖的延遲時間,默認是 4s ,也就是說偏向鎖是默認開啟的,但是在啟動程序后有 4s 的延遲-XX:+UseBiasedLocking
開啟偏向鎖,默認是 開啟的。程序中想要使用偏向鎖:
-XX:BiasedLockingStartupDelay=0
代碼演示沒有加任何 JVM 參數(shù)的情況:
未加任何參數(shù)情況下,鎖的狀態(tài)標志位是 000,因為偏向鎖有延遲時間。
代碼演示增加JVM參數(shù)
-XX:BiasedLockingStartupDelay=0
此時鎖狀態(tài)標志位為 101——偏向鎖
多線程環(huán)境下當(dāng)有另外線程逐步來競爭鎖的時候,就不能再使用偏向鎖了,要升級為輕量級鎖
競爭線程嘗試CAS更新對象頭失敗,會等待到全局安全點(此時不會執(zhí)行任何代碼)撤銷偏向鎖。
撤銷鎖的流程:
偏向鎖使用一種等到競爭出現(xiàn)才釋放鎖的機制,只有當(dāng)其他線程競爭鎖時,持有偏向鎖的原來線程才會被撤銷。
撤銷需要等待全局安全點(該時間點上沒有字節(jié)碼正在執(zhí)行),同時檢查持有偏向鎖的線程是否還在執(zhí)行:
① 第一個線程正在執(zhí)行synchronized方法(處于同步塊),它還沒有執(zhí)行完,其它線程來搶奪,該偏向鎖會被取消掉并出現(xiàn)鎖升級。此時輕量級鎖由原持有偏向鎖的線程持有,繼續(xù)執(zhí)行其同步代碼,而正在競爭的線程會進入自旋等待獲得該輕量級鎖。
② 第一個線程執(zhí)行完成synchronized方法(退出同步塊),則將對象頭設(shè)置成無鎖狀態(tài)并撤銷偏向鎖,重新偏向 。
總結(jié)輕量級鎖jdk15 以后偏向鎖已經(jīng)被廢棄了,原因就是:偏向鎖維護起來耗時耗神
多線程競爭,但是任意時刻最多只有一個線程競爭(其他線程自旋),即不存在鎖競爭太過激烈的情況,也就沒有線程阻寨
64 位圖:
輕量級鎖的獲取輕量級鎖是為了在線程近乎交替執(zhí)行同步塊時提高性能。
主要目的: 在沒有多線程競爭的前提下,通過CAS減少重量級鎖使用操作系統(tǒng)互斥鎖產(chǎn)生的性能消耗,說白了先自旋再阻塞。
升級時機: 當(dāng)關(guān)閉偏向鎖功能或多線程競爭偏向鎖會導(dǎo)致偏向鎖升級為輕量級鎖
假如線程A已經(jīng)拿到鎖,這時線程B又來搶該對象的鎖,由于該對象的鎖已經(jīng)被線程A拿到,當(dāng)前該鎖已是偏向鎖了。
而線程B在爭搶時發(fā)現(xiàn)對象頭Mark Word中的線程ID不是線程B自己的線程ID(而是線程A),那線程B就會進行CAS操作希望能獲得鎖。
此時線程B操作中有兩種情況:
如果鎖獲取成功,直接替換Mark Word中的線程ID為B自己的ID(A → B),重新偏向于其他線程(即將偏向鎖交給其他線程,相當(dāng)于當(dāng)前線程"被"釋放了鎖),該鎖會保持偏向鎖狀態(tài),A線程Over,B線程上位;
如果鎖獲取失敗,則偏向鎖升級為輕量級鎖,此時輕量級鎖由原持有偏向鎖的線程持有,繼續(xù)執(zhí)行其同步代碼,而正在競爭的線程B會進入自旋等待獲得該輕量級鎖。
輕量級鎖的加鎖
JVM會為每個線程在當(dāng)前線程的棧幀中創(chuàng)建用于存儲鎖記錄的空間,官方成為Displaced Mark Word
。若一個線程獲得鎖時發(fā)現(xiàn)是輕量級鎖,會把鎖的MarkWord復(fù)制到自己的Displaced Mark Word里面。然后線程嘗試用CAS將鎖的MarkWord替換為指向鎖記錄的指針。如果成功,當(dāng)前線程獲得鎖,如果失敗,表示Mark Word已經(jīng)被替換成了其他線程的鎖記錄,說明在與其它線程競爭鎖,當(dāng)前線程就嘗試使用自旋來獲取鎖。
自旋CAS:不斷嘗試去獲取鎖,能不升級就不往上捅,盡量不要阻寨
輕量級鎖的釋放
在釋放鎖時,當(dāng)前線程會使用CAS操作將Displaced Mark Word的內(nèi)容復(fù)制回鎖的Mark Word里面。如果沒有發(fā)生競爭,那么這個復(fù)制的操作會成功。如果有其他線程因為自旋多次導(dǎo)致輕量級鎖升級成了重量級鎖,那么CAS操作會失敗,此時會釋放鎖并喚醒被阻寨的線程。
代碼演示輕量級鎖自適應(yīng)鎖設(shè)置JVM參數(shù):
-XX:-UseBiasedLocking
關(guān)閉偏向鎖
自適應(yīng)鎖就是自適應(yīng)的自旋鎖,自旋的時間不是固定時間,而是由前一次在同一個鎖上的自旋時間和鎖的持有者狀態(tài)來決定。
簡單來說就是當(dāng)使用輕量級鎖時,線程自旋的次數(shù)太多或達到一定程度后,就會自適應(yīng)
輕量級鎖和偏向鎖的區(qū)別有大量的線程參與鎖的競爭,沖突性很高
64位圖:
重鎖原理
Java中synchronized的重量級鎖,是基于進入和退出Monitor對象實現(xiàn)的。在編譯時會將同步塊的開始位置插入monitor enter指令,在結(jié)束位置插入monitor exit指令。
當(dāng)線程執(zhí)行到monitor enter指令時,會嘗試獲取對象所對應(yīng)的Monitor所有權(quán),如果獲取到了,即獲取到了鎖,會在Monitor的owner中存放當(dāng)前線程的id,這樣它將處于鎖定狀態(tài),除非退出同步塊,否則其他線程無法獲取到這個Monitor。
代碼演示:
總結(jié) 鎖升級與hashCode的關(guān)系鎖升級為輕量級或重量級鎖后,Mark Word中保存的分別是線程棧幀里的鎖記錄指針和重量級鎖指針,己經(jīng)沒有位置再保存哈希碼,GC年齡了,那么這些信息被移動到哪里去了呢?
在無鎖狀態(tài)下,Mark Word中可以存儲對象的identity hash code值。當(dāng)對象的hashCode()方法第一次被調(diào)用時,JVM會生成對應(yīng)的identity hash code值并將該值存儲到Mark Word中。
對于偏向鎖,在線程獲取偏向鎖時,會用Thread ID和epoch值覆蓋identity hash code所在的位置。如果一個對象的hashCode()方法己經(jīng)被調(diào)用過一次之后,這個對象不能被設(shè)置偏向鎖。因為如果可以的化,那Mark Word中的identity hash code必然會被偏向線程Id給覆蓋,這就會造成同一個對象前后兩次調(diào)用hashCode()方法得到的結(jié)果不一致。
升級為輕量級鎖時,JVM會在當(dāng)前線程的棧幀中創(chuàng)建一個鎖記錄(Lock Record)空間,用于存儲鎖對象的Mark Word拷貝,該拷貝中可以包含identity hash code,所以輕量級鎖可以和identity hash code共存,哈希碼和GC年齡自然保存在此,釋放鎖后會將這些信息寫回到對象頭。
升級為重量級鎖后,Mark Word保存的重量級鎖指針,代表重量級鎖的ObjectMonitor類里有字段記錄非加鎖狀態(tài)下的Mark Word,鎖釋放后也會將信息寫回到對象頭。
輕量級鎖和重量級鎖都有額外的空間來保存 MarkWord (hashCode,GC年齡…)中的信息,當(dāng)鎖釋放時,將信息寫回 MarkWord中。
一:當(dāng)對象獲取偏向鎖之前計算過hashCode值,就無法進入偏向鎖,而是跳過偏向鎖進入輕量級鎖(代碼演示一)。
二:當(dāng)對象處于偏向鎖時,此時計算hashCode值,偏向鎖會膨脹為重量級鎖(代碼演示二)。
代碼演示一:
代碼演示二:
各類鎖的優(yōu)缺點synchronized鎖升級過程總結(jié):一句話,就是先自旋,不行再阻塞。
實際上是把之前的悲觀鎖(重量級鎖)變成在一定條件下使用偏向鎖以及使用輕量級(自旋鎖CAS)的形式
synchronized在修飾方法和代碼塊在字節(jié)碼上實現(xiàn)方式有很大差異,但是內(nèi)部實現(xiàn)還是基于對象頭的MarkWord來實現(xiàn)的。
JDK1.6之前synchronized使用的是重量級鎖,JDK1.6之后進行了優(yōu)化,擁有了無鎖->偏向鎖->輕量級鎖->重量級鎖的升級過程,而不是無論什么情況都使用重量級鎖。
學(xué)過jvm的應(yīng)該清楚,基于逃逸分析的同步省略,就是省略代碼塊中無用的 synchronized ,也是jvm一種優(yōu)化手段。這個鎖消除和同步策略一樣
代碼演示:
public class LockClearUPDemo {static Object objectLock = new Object();//正常的
public void m1() {//鎖消除,JIT會無視它,
// 每個對象都有 o 對象,加鎖已經(jīng)無意義了;
Object o = new Object();
synchronized (o) {System.out.println("-----hello LockClearUPDemo" + "\t" + o.hashCode() + "\t" + objectLock.hashCode());
}
}
public static void main(String[] args) {LockClearUPDemo demo = new LockClearUPDemo();
for (int i = 1; i<= 10; i++) {new Thread(() ->{demo.m1();
}, String.valueOf(i)).start();
}
}
}
鎖粗化假如方法中首位相接,前后相鄰的都是同一個鎖對象,那JIT編譯器就會把這幾個synchronized塊合并成一個大塊,
加粗加大范圍,一次申請使用即可,避免次次都申請和釋放鎖,提升了性能
代碼演示:
public class LockBigDemo
{static Object objectLock = new Object();
public static void main(String[] args)
{new Thread(() ->{synchronized (objectLock) {System.out.println("11111");
}
synchronized (objectLock) {System.out.println("22222");
}
synchronized (objectLock) {System.out.println("33333");
}
// 以上代碼會被替換成以下代碼》。。。
synchronized (objectLock) {System.out.println("11111");
System.out.println("22222");
System.out.println("33333");
}
},"a").start();
}
}
各位彭于晏,如有收獲點個贊不過分吧…???
gongzhonghao 回復(fù) [JUC] 獲取MarkDown筆記
你是否還在尋找穩(wěn)定的海外服務(wù)器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機房具備T級流量清洗系統(tǒng)配攻擊溯源,準確流量調(diào)度確保服務(wù)器高可用性,企業(yè)級服務(wù)器適合批量采購,新人活動首月15元起,快前往官網(wǎng)查看詳情吧