synchronized原理_synchronized关键字的作用、原理以及锁优化
synchronized簡(jiǎn)介
synchronized塊是Java提供的一種原子性內(nèi)置鎖,Java中的每個(gè)對(duì)象都可以隱式地扮演一個(gè)用于同步的鎖的角色。這些Java內(nèi)置的使用者看不到的鎖被稱為內(nèi)部鎖(intrinsic locks),也叫作監(jiān)視器鎖(monitor locks)。
線程在進(jìn)入synchronized代碼塊會(huì)自動(dòng)獲取內(nèi)部鎖,這時(shí)候其他線程訪問(wèn)該同步代碼塊時(shí)會(huì)被阻塞掛起。
拿到內(nèi)部鎖的線程會(huì)在正常退出同步代碼塊或者拋出異常后或者在同步塊內(nèi)調(diào)用該內(nèi)置鎖資源的wait系列方法時(shí)釋放該鎖。
內(nèi)部鎖在Java中扮演了互斥鎖(mutual exclusion lock,也稱作mutex)的角色,意味著至多只有一個(gè)線程可以擁有鎖,當(dāng)線程A嘗試請(qǐng)求一個(gè)被線程B占有的鎖時(shí),線程A必須等待或者阻塞,直到B釋放它。如果B永遠(yuǎn)不釋放鎖,A將永遠(yuǎn)等下去。
除了用于線程同步、確保線程安全外,關(guān)鍵字synchronized還可以保證線程間的可見(jiàn)性和有序性。
synchronized 使用的三種方式
- 修飾代碼塊: 指定加鎖對(duì)象,對(duì)給定對(duì)象加鎖,進(jìn)入同步代碼庫(kù)前要獲得給定對(duì)象的鎖。
- 修飾實(shí)例方法: 作用于當(dāng)前對(duì)象實(shí)例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前對(duì)象實(shí)例的鎖
- 修飾靜態(tài)方法: 也就是給當(dāng)前類加鎖,會(huì)作用于類的所有對(duì)象實(shí)例,因?yàn)殪o態(tài)成員不屬于任何一個(gè)實(shí)例對(duì)象,是類成員( static 表明這是該類的一個(gè)靜態(tài)資源,不管new了多少個(gè)對(duì)象,只有一份)。所以如果一個(gè)線程 A 調(diào)用一個(gè)實(shí)例對(duì)象的非靜態(tài) synchronized 方法,而線程 B 需要調(diào)用這個(gè)實(shí)例對(duì)象所屬類的靜態(tài) synchronized 方法,是允許的,不會(huì)發(fā)生互斥現(xiàn)象,因?yàn)樵L問(wèn)靜態(tài) synchronized 方法占用的鎖是當(dāng)前類的鎖,而訪問(wèn)非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實(shí)例對(duì)象鎖。
實(shí)例1:修飾代碼塊:
public class AccountingSync implements Runnable{static final AccountingSync instance = new AccountingSync();static int i = 0;@Overridepublic void run() {for (int j = 0; j < 10000000; j++) {synchronized (instance) {i++;}}} }上述代碼將synchronized作用于一個(gè)給定對(duì)象instance,因此,每次當(dāng)線程進(jìn)入關(guān)鍵字synchronized包裹的代碼段,就都會(huì)要求請(qǐng)求instance實(shí)例的鎖。如果當(dāng)前有其他線程正持有這把鎖,那么新到的線程就必須等待。這樣,就保證了每次只能有一個(gè)線程執(zhí)行i++操作。
示例2:修飾實(shí)例方法:
public class AccountingSync2 implements Runnable{static final AccountingSync2 instance = new AccountingSync2();static int i = 0;public synchronized void increase(){i++;}@Overridepublic void run() {for (int j = 0; j < 10000000; j++) {increase();}} }public static void main(String[] args) throws InterruptedException {//顯示創(chuàng)建線程,兩個(gè)線程都指向同一個(gè)Runnable接口實(shí)例(instance對(duì)象)Thread t1 = new Thread(instance);Thread t2 = new Thread(instance);t1.start();t2.start();t1.join();t2.join();}上述代碼中,關(guān)鍵字 synchronized 作用于一個(gè)實(shí)例方法。就是說(shuō)在進(jìn)入increase()方法前,線程必須獲得當(dāng)前對(duì)象實(shí)例的鎖,在本例中就是instance。
這里特定寫(xiě)出了main()方法,在main()方法,我們顯示的創(chuàng)建了兩個(gè)線程,并且將兩個(gè)線程都指向同一個(gè)Runnable接口實(shí)例(instance對(duì)象)。這樣才能保證兩個(gè)線程在工作時(shí),能夠關(guān)注到同一個(gè)對(duì)象鎖上去,從而保證線程安全。
錯(cuò)誤示例2.1:
public class AccountingSync2 implements Runnable{static final AccountingSync2 instance = new AccountingSync2();static int i = 0;public synchronized void increase(){i++;}@Overridepublic void run() {for (int j = 0; j < 10000000; j++) {increase();}}public static void main(String[] args) throws InterruptedException {//錯(cuò)誤用法Thread t1 = new Thread(new AccountingSync2);Thread t2 = new Thread(new AccountingSync2);t1.start();t2.start();t1.join();t2.join();} }示例3:修飾靜態(tài)方法
錯(cuò)誤示例2.1可以簡(jiǎn)單的修改一下就可以正常工作,將increase()方法修改如下:
public static synchronized void increase(){i++;}這樣,即使兩個(gè)線程指向不同的Runnable對(duì)象,也可以正確同步。因?yàn)榉椒煺?qǐng)求的是當(dāng)前類的鎖,而非當(dāng)前實(shí)例。
synchronized底層原理
修改代碼塊
public class SynchronizedDemo {public void method() {synchronized (this) {System.out.println("synchronized 代碼塊");}} }通過(guò) JDK 自帶的 javap 命令查看 SynchronizedDemo 類的相關(guān)字節(jié)碼信息:首先切換到類的對(duì)應(yīng)目錄執(zhí)行 javac SynchronizedDemo.java 命令生成編譯后的 .class 文件,然后執(zhí)行javap -c -s -v -l SynchronizedDemo.class。
synchronized 同步語(yǔ)句塊經(jīng)過(guò)Javac編譯后,會(huì)在同步塊的前后分別形成 monitorenter 和 monitorexit 指令。這兩個(gè)字節(jié)碼指令都需要一個(gè)reference類型的參數(shù)來(lái)指明要鎖定和解鎖的對(duì)象。
根據(jù)《Java虛擬機(jī)規(guī)范》的要求,在執(zhí)行 monitorenter指令時(shí),首先要去嘗試獲取對(duì)象的鎖。如果這個(gè)對(duì)象沒(méi)被鎖定,或者當(dāng)前線程已經(jīng)持有了那個(gè)對(duì)象的鎖,就把鎖的計(jì)數(shù)器的值增加一,而在執(zhí)行 monitorexit 指令時(shí)會(huì)將鎖計(jì)數(shù)器的值減一。一旦計(jì)數(shù)器的值為零,鎖隨即就被釋放了。如果獲取對(duì)象鎖失敗,那當(dāng)前線程就應(yīng)當(dāng)被阻塞等待,直到請(qǐng)求鎖定的對(duì)象被持有它的線程釋放為止。
修飾方法的情況
public class SynchronizedDemo2 {public synchronized void method() {System.out.println("synchronized 方法");} }synchronized 修飾的方法并沒(méi)有 monitorenter 指令和 monitorexit 指令,取得代之的確實(shí)是 ACC_SYNCHRONIZED 標(biāo)識(shí),該標(biāo)識(shí)指明了該方法是一個(gè)同步方法,JVM 通過(guò)該 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志來(lái)辨別一個(gè)方法是否聲明為同步方法,從而執(zhí)行相應(yīng)的同步調(diào)用。
JDK1.6之后的鎖優(yōu)化
synchronized 是Java語(yǔ)言中的一個(gè)重量級(jí)的操作,在主流Java虛擬機(jī)實(shí)現(xiàn)中,Java的線程時(shí)映射到操作系統(tǒng)的原生內(nèi)核線程之上的,如果要阻塞或喚醒一條線程,則需要操作系統(tǒng)來(lái)幫忙完成,這就不可避免地陷入用戶態(tài)到核心態(tài)的轉(zhuǎn)換中,進(jìn)行這種狀態(tài)轉(zhuǎn)換需要很多的處理器時(shí)間。尤其是對(duì)于代碼特別簡(jiǎn)單的同步塊(譬如被 synchronized修飾的getter()或setter()方法),狀態(tài)轉(zhuǎn)消耗的時(shí)間甚至?xí)扔脩舸a本身執(zhí)行的時(shí)間還要長(zhǎng)。
JDK5升級(jí)到JDK6,HotPot虛擬機(jī)開(kāi)發(fā)團(tuán)隊(duì)在這個(gè)版本花了大量資源去實(shí)現(xiàn)各種鎖優(yōu)化技術(shù),如適應(yīng)性自旋鎖(Adaptive Spinning)、鎖消除(Lock Elimination)、鎖粗化(Lock Coarsening)、輕量級(jí)鎖(Lightweight Locking)、偏向鎖(Biased Locking)等,這些技術(shù)都是為了提升鎖的效率。
偏向鎖
偏向鎖的目的是消除數(shù)據(jù)在無(wú)競(jìng)爭(zhēng)情況下的同步原語(yǔ),進(jìn)一步提高程序的運(yùn)行性能。
偏向鎖的核心思想是如果一個(gè)線程獲得了鎖,那么鎖就進(jìn)入偏向模式。當(dāng)這個(gè)線程再次請(qǐng)求鎖時(shí),無(wú)須再做任何同步操作。這樣就節(jié)省了大量有關(guān)鎖申請(qǐng)的操作,從而提高了程序性能。因此,對(duì)于幾乎沒(méi)有鎖競(jìng)爭(zhēng)的場(chǎng)合,偏向鎖有比較好的優(yōu)化效果,因?yàn)檫B續(xù)多次極有可能是同一個(gè)線程請(qǐng)求相同的鎖。而對(duì)于鎖競(jìng)爭(zhēng)比較激烈的場(chǎng)合,其效果不佳,因?yàn)檫@時(shí)偏向模式會(huì)失效。使用Java虛擬機(jī)參數(shù)-XX:+UseBiasedLocking可以開(kāi)啟偏向鎖。
要理解偏向鎖以及后面的輕量級(jí)鎖的原理,必須了解HotSpot虛擬機(jī)的對(duì)象頭信息。HotSpot虛擬機(jī)的對(duì)象頭分為兩部分
- 第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡(Generational GC Age)等。這部分?jǐn)?shù)據(jù)的長(zhǎng)度在32位和64位的Java虛擬機(jī)中分別占用32個(gè)或64個(gè)比特,官方稱它為“Mark Word”。這部分是實(shí)現(xiàn)偏向鎖和輕量級(jí)鎖的關(guān)鍵。
- 第二部分用于存儲(chǔ)指向方法區(qū)對(duì)象類型數(shù)據(jù)的指針,如果是數(shù)組對(duì)象,還會(huì)有一個(gè)額外的部分用于存儲(chǔ)數(shù)組長(zhǎng)度。
這里重點(diǎn)介紹Mark Word,它被設(shè)計(jì)成一個(gè)非固定的動(dòng)態(tài)數(shù)據(jù)結(jié)構(gòu),以盡可能的減少占用空間。鎖的狀態(tài)不同,存儲(chǔ)的內(nèi)容也不同。以下是不同狀態(tài)下對(duì)象頭的存儲(chǔ)內(nèi)容示意圖:
(圖源《深入理解Java虛擬機(jī)》)
當(dāng)鎖對(duì)象第一次被線程獲取的時(shí)候,虛擬機(jī)會(huì)把對(duì)象頭的標(biāo)志位設(shè)置為“01”、把偏向模式設(shè)置為“1”,表示進(jìn)入偏向模式,同時(shí)使用CAS操作把獲得這個(gè)鎖的線程的ID記錄在對(duì)象的Mark Word中。如果CAS操作成功,持有偏向鎖的線程以后在此進(jìn)入這個(gè)鎖的同步塊時(shí),虛擬機(jī)都可以不再進(jìn)行任何同步操作(例如加鎖、解鎖以及對(duì)Mark Word的更新操作等。)
一旦有另外一個(gè)線程來(lái)嘗試獲取這個(gè)鎖,偏向模式馬上就會(huì)結(jié)束。根據(jù)鎖對(duì)象目前是否處于被鎖定狀態(tài)決定是否撤銷偏向(偏向模式設(shè)置為“0”)。撤銷后標(biāo)志位恢復(fù)到未鎖定(標(biāo)志位為“01”或輕量級(jí)鎖定(標(biāo)志位為“00”)的狀態(tài))。
輕量級(jí)鎖
如果偏向鎖失敗,虛擬機(jī)也不會(huì)立即掛起線程,它還會(huì)使用一種稱為輕量級(jí)鎖的優(yōu)化手段。
區(qū)別于重量級(jí)鎖使用操作系統(tǒng)互斥量來(lái)實(shí)現(xiàn),輕量級(jí)鎖的加鎖和解鎖都使用CAS來(lái)完成。
輕量級(jí)鎖的加鎖
線程在執(zhí)行同步塊之前,JVM會(huì)先在當(dāng)前線程的楨棧中創(chuàng)建用于存儲(chǔ)鎖記錄的空間,并將對(duì)象頭中的Mark Word復(fù)制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對(duì)象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當(dāng)前線程獲得鎖,將對(duì)象Mark Word的鎖標(biāo)志位轉(zhuǎn)變?yōu)椤?0”,表示此對(duì)象處于輕量級(jí)鎖狀態(tài)。
如果失敗,表示其他線程搶先爭(zhēng)奪到了鎖,輕量級(jí)鎖就不再有效,膨脹為重量級(jí)鎖,并嘗試使用自旋來(lái)獲取鎖。
輕量級(jí)鎖解鎖
輕量級(jí)鎖解鎖時(shí),會(huì)使用CAS操作將Displaced Mark Word 替換回到對(duì)象頭,如果成功,則表示沒(méi)有競(jìng)爭(zhēng)發(fā)生。如果失敗,表示當(dāng)前鎖正在競(jìng)爭(zhēng),鎖就會(huì)膨脹成重量級(jí)鎖。
輕量級(jí)鎖能提升性能的依據(jù)是“對(duì)于絕大部分鎖,在整個(gè)同步周期內(nèi)都不存在競(jìng)爭(zhēng)”這一經(jīng)驗(yàn)法則。如果沒(méi)有競(jìng)爭(zhēng),輕量級(jí)鎖便通過(guò)CAS操作避免了使用互斥量的開(kāi)銷;但如果存在鎖競(jìng)爭(zhēng),除了互斥量的本身開(kāi)銷外,還額外發(fā)生了CAS操作的開(kāi)銷。因此在有競(jìng)爭(zhēng)的情況下,輕量級(jí)鎖反而比傳統(tǒng)的重量級(jí)鎖更慢。
自旋鎖和自適應(yīng)自旋
輕量級(jí)鎖失敗后,虛擬機(jī)為了避免線程真實(shí)地在操作系統(tǒng)層面掛起,還會(huì)進(jìn)行一項(xiàng)稱為自旋鎖的優(yōu)化手段。
掛起和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成,如果加鎖失敗便簡(jiǎn)單粗暴的掛起線程可能得不償失。因?yàn)樘摂M機(jī)的開(kāi)發(fā)團(tuán)隊(duì)注意到在許多應(yīng)用上,共享數(shù)據(jù)的鎖定狀態(tài)只會(huì)持續(xù)很短的一段時(shí)間。為了這段時(shí)間去掛起和恢復(fù)線程并不值得。
這時(shí),虛擬機(jī)會(huì)讓沒(méi)請(qǐng)求到鎖的線程做幾個(gè)空循環(huán)(自旋),這時(shí)線程并不會(huì)放棄處理器的執(zhí)行時(shí)間,,在經(jīng)過(guò)若干次循環(huán)后,如果可以得到鎖,那便進(jìn)入同步塊。
自旋鎖在 JDK1.6 之前其實(shí)就已經(jīng)引入了,不過(guò)是默認(rèn)關(guān)閉的,需要通過(guò)--XX:+UseSpinning參數(shù)來(lái)開(kāi)啟。JDK1.6及1.6之后,就改為默認(rèn)開(kāi)啟的了。需要注意的是:自旋等待不能完全替代阻塞,因?yàn)樗€是要占用處理器時(shí)間。如果鎖被占用的時(shí)間短,那么效果當(dāng)然就很好了!反之,自旋等待的時(shí)間必須要有限度。如果自旋超過(guò)了限定次數(shù)任然沒(méi)有獲得鎖,就應(yīng)該掛起線程。自旋次數(shù)的默認(rèn)值是10次,用戶可以修改--XX:PreBlockSpin來(lái)更改。
另外,在 JDK1.6 中引入了自適應(yīng)的自旋鎖。自適應(yīng)的自旋鎖帶來(lái)的改進(jìn)就是:自旋的時(shí)間不在固定了,而是由前一次在同一個(gè)鎖上的自旋時(shí)間以及鎖的擁有者的狀態(tài)來(lái)決定。
如果在同一個(gè)鎖對(duì)象上,自旋等待剛成功獲得過(guò)鎖,并且持有鎖的線程正在運(yùn)行,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也很可能再次成功,進(jìn)而允許自旋等待更長(zhǎng)的時(shí)間。另一方面,如果對(duì)于某個(gè)鎖,自旋很少成功獲得鎖,那以后要獲取這個(gè)鎖時(shí)可能直接省略掉自旋過(guò)程。
鎖消除
鎖消除是一更加徹底的優(yōu)化,指虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí),對(duì)一些代碼要求同步,但是對(duì)被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行消除。
可是,既然不存在競(jìng)爭(zhēng),為什么程序員還要加鎖呢?其實(shí)有許多同步措施并不是程序員自己加入的,而是一些內(nèi)置的API,框架加入的。比如下面的代碼:
public String[] concatString() {Vector<String> vector = new Vector<>();for (int j = 0; j < 100; j++) {vector.add(Integer.toString(i));}return vector.toArray(new String[]{}); }在這段代碼中,變量vector只在concatString()方法中使用,它是局部變量。局部變量是在線程棧上分配的,屬于線程私有的數(shù)據(jù),不會(huì)被其他線程訪問(wèn)到。在這種情況下,Vector內(nèi)部所有加鎖同步都是沒(méi)有必要的,這時(shí)虛擬機(jī)會(huì)將這些無(wú)用的鎖操作去除。
鎖消除的主要判定依據(jù)是逃逸分析,就是觀察某一個(gè)變量是否會(huì)逃出某一個(gè)作用域,如果判斷到一段代碼中,在堆上所有數(shù)據(jù)都不會(huì)逃逸出去被其他線程訪問(wèn)到,那就可以把他們當(dāng)做棧上數(shù)據(jù)對(duì)待,認(rèn)為它們是線程私有的,同步加鎖自然不用進(jìn)行。
逃逸分析必須在-server模式下進(jìn)行,可以使用-XX:+DoEscapeAnalysis參數(shù)打開(kāi)逃逸分析。使用-XX:+EliminateLocks參數(shù)可以打開(kāi)鎖消除。
鎖粗化
原則上,我們?cè)诰帉?xiě)代碼的時(shí)候,總是推薦將同步塊的作用范圍限制得盡可能小,這樣可以讓同步操作所需的時(shí)間盡可能少,即使存在鎖競(jìng)爭(zhēng),等待鎖的線程也能盡可能塊地拿到鎖。
大多數(shù)情況下,上面的原則是正確的,但是如果一系列的連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體中,那會(huì)導(dǎo)致不必要的性能損耗。
如果虛擬機(jī)探測(cè)到這樣的操作,就會(huì)把加鎖同步的返回?cái)U(kuò)展(粗化)到整個(gè)操作序列的外部,這樣只需要加鎖一次就可以了。
總結(jié)
以上是生活随笔為你收集整理的synchronized原理_synchronized关键字的作用、原理以及锁优化的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: vs code 开发企业级python_
- 下一篇: 单臂路由配置实验同一交换机上vlan间p