本篇內(nèi)容主要講解“java中Synchronized的原理及應(yīng)用”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“java中Synchronized的原理及應(yīng)用”吧!
成都創(chuàng)新互聯(lián)公司服務(wù)項(xiàng)目包括武穴網(wǎng)站建設(shè)、武穴網(wǎng)站制作、武穴網(wǎng)頁制作以及武穴網(wǎng)絡(luò)營銷策劃等。多年來,我們專注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術(shù)優(yōu)勢、行業(yè)經(jīng)驗(yàn)、深度合作伙伴關(guān)系等,向廣大中小型企業(yè)、政府機(jī)構(gòu)等提供互聯(lián)網(wǎng)行業(yè)的解決方案,武穴網(wǎng)站推廣取得了明顯的社會效益與經(jīng)濟(jì)效益。目前,我們服務(wù)的客戶以成都為中心已經(jīng)輻射到武穴省份的部分城市,未來相信會繼續(xù)擴(kuò)大服務(wù)區(qū)域并繼續(xù)獲得客戶的支持與信任!
Java并發(fā)編程系列第二篇Synchronized
,文章風(fēng)格依然是圖文并茂,通俗易懂,本文帶讀者們由淺入深理解Synchronized
,讓讀者們也能與面試官瘋狂對線。
在并發(fā)編程中Synchronized
一直都是元老級的角色,Jdk 1.6
以前大家都稱呼它為重量級鎖,相對于J U C
包提供的Lock
,它會顯得笨重,不過隨著Jdk 1.6
對Synchronized
進(jìn)行各種優(yōu)化后,Synchronized
性能已經(jīng)非??炝?。
Synchronized
是Java
提供的同步關(guān)鍵字,在多線程場景下,對共享資源代碼段進(jìn)行讀寫操作(必須包含寫操作,光讀不會有線程安全問題,因?yàn)樽x操作天然具備線程安全特性),可能會出現(xiàn)線程安全問題,我們可以使用Synchronized
鎖定共享資源代碼段,達(dá)到互斥(mutualexclusion
)效果,保證線程安全。
共享資源代碼段又稱為臨界區(qū)(critical section
),保證臨界區(qū)互斥,是指執(zhí)行臨界區(qū)(critical section
)的只能有一個(gè)線程執(zhí)行,其他線程阻塞等待,達(dá)到排隊(duì)效果。
Synchronized
的食用方式有三種
修飾普通函數(shù),監(jiān)視器鎖(monitor
)便是對象實(shí)例(this
)
修飾靜態(tài)靜態(tài)函數(shù),視器鎖(monitor
)便是對象的Class
實(shí)例(每個(gè)對象只有一個(gè)Class
實(shí)例)
修飾代碼塊,監(jiān)視器鎖(monitor
)是指定對象實(shí)例
普通函數(shù)使用Synchronized
的方式很簡單,在訪問權(quán)限修飾符與函數(shù)返回類型間加上Synchronized
。
多線程場景下,thread
與threadTwo
兩個(gè)線程執(zhí)行incr
函數(shù),incr
函數(shù)作為共享資源代碼段被多線程讀寫操作,我們將它稱為臨界區(qū),為了保證臨界區(qū)互斥,使用Synchronized
修飾incr
函數(shù)即可。
public class SyncTest { private int j = 0; /** * 自增方法 */ public synchronized void incr(){ //臨界區(qū)代碼--start for (int i = 0; i < 10000; i++) { j++; } //臨界區(qū)代碼--end } public int getJ() { return j; } } public class SyncMain { public static void main(String[] agrs) throws InterruptedException { SyncTest syncTest = new SyncTest(); Thread thread = new Thread(() -> syncTest.incr()); Thread threadTwo = new Thread(() -> syncTest.incr()); thread.start(); threadTwo.start(); thread.join(); threadTwo.join(); //最終打印結(jié)果是20000,如果不使用synchronized修飾,就會導(dǎo)致線程安全問題,輸出不確定結(jié)果 System.out.println(syncTest.getJ()); } }
代碼十分簡單,incr
函數(shù)被synchronized
修飾,函數(shù)邏輯是對j
進(jìn)行10000
次累加,兩個(gè)線程執(zhí)行incr
函數(shù),最后輸出j
結(jié)果。
被synchronized
修飾函數(shù)我們簡稱同步函數(shù),線程執(zhí)行稱同步函數(shù)前,需要先獲取監(jiān)視器鎖,簡稱鎖,獲取鎖成功才能執(zhí)行同步函數(shù),同步函數(shù)執(zhí)行完后,線程會釋放鎖并通知喚醒其他線程獲取鎖,獲取鎖失敗「則阻塞并等待通知喚醒該線程重新獲取鎖」,同步函數(shù)會以this
作為鎖,即當(dāng)前對象,以上面的代碼段為例就是syncTest
對象。
線程thread
執(zhí)行syncTest.incr()
前
線程thread
獲取鎖成功
線程threadTwo
執(zhí)行syncTest.incr()
前
線程threadTwo
獲取鎖失敗
線程threadTwo
阻塞并等待喚醒
線程thread
執(zhí)行完syncTest.incr()
,j
累積到10000
線程thread
釋放鎖,通知喚醒threadTwo
線程獲取鎖
線程threadTwo
獲取鎖成功
線程threadTwo
執(zhí)行完syncTest.incr()
,j
累積到20000
線程threadTwo
釋放鎖
靜態(tài)函數(shù)顧名思義,就是靜態(tài)的函數(shù),它使用Synchronized
的方式與普通函數(shù)一致,唯一的區(qū)別是鎖的對象不再是this
,而是Class
對象。
多線程執(zhí)行Synchronized
修飾靜態(tài)函數(shù)代碼段如下。
public class SyncTest { private static int j = 0; /** * 自增方法 */ public static synchronized void incr(){ //臨界區(qū)代碼--start for (int i = 0; i < 10000; i++) { j++; } //臨界區(qū)代碼--end } public static int getJ() { return j; } } public class SyncMain { public static void main(String[] agrs) throws InterruptedException { Thread thread = new Thread(() -> SyncTest.incr()); Thread threadTwo = new Thread(() -> SyncTest.incr()); thread.start(); threadTwo.start(); thread.join(); threadTwo.join(); //最終打印結(jié)果是20000,如果不使用synchronized修飾,就會導(dǎo)致線程安全問題,輸出不確定結(jié)果 System.out.println(SyncTest.getJ()); } }
Java
的靜態(tài)資源可以直接通過類名調(diào)用,靜態(tài)資源不屬于任何實(shí)例對象,它只屬于Class
對象,每個(gè)Class
在J V M
中只有唯一的一個(gè)Class
對象,所以同步靜態(tài)函數(shù)會以Class
對象作為鎖,后續(xù)獲取鎖、釋放鎖流程都一致。
前面介紹的普通函數(shù)與靜態(tài)函數(shù)粒度都比較大,以整個(gè)函數(shù)為范圍鎖定,現(xiàn)在想把范圍縮小、靈活配置,就需要使用代碼塊了,使用{}
符號定義范圍給Synchronized
修飾。
下面代碼中定義了syncDbData
函數(shù),syncDbData
是一個(gè)偽同步數(shù)據(jù)的函數(shù),耗時(shí)2
秒,并且邏輯不涉及共享資源讀寫操作(非臨界區(qū)),另外還有兩個(gè)函數(shù)incr
與incrTwo
,都是在自增邏輯前執(zhí)行了syncDbData
函數(shù),只是使用Synchronized
的姿勢不同,一個(gè)是修飾在函數(shù)上,另一個(gè)是修飾在代碼塊上。
public class SyncTest { private static int j = 0; /** * 同步庫數(shù)據(jù),比較耗時(shí),代碼資源不涉及共享資源讀寫操作。 */ public void syncDbData() { System.out.println("db數(shù)據(jù)開始同步------------"); try { //同步時(shí)間需要2秒 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("db數(shù)據(jù)開始同步完成------------"); } //自增方法 public synchronized void incr() { //start--臨界區(qū)代碼 //同步庫數(shù)據(jù) syncDbData(); for (int i = 0; i < 10000; i++) { j++; } //end--臨界區(qū)代碼 } //自增方法 public void incrTwo() { //同步庫數(shù)據(jù) syncDbData(); synchronized (this) { //start--臨界區(qū)代碼 for (int i = 0; i < 10000; i++) { j++; } //end--臨界區(qū)代碼 } } public int getJ() { return j; } } public class SyncMain { public static void main(String[] agrs) throws InterruptedException { //incr同步方法執(zhí)行 SyncTest syncTest = new SyncTest(); Thread thread = new Thread(() -> syncTest.incr()); Thread threadTwo = new Thread(() -> syncTest.incr()); thread.start(); threadTwo.start(); thread.join(); threadTwo.join(); //最終打印結(jié)果是20000 System.out.println(syncTest.getJ()); //incrTwo同步塊執(zhí)行 thread = new Thread(() -> syncTest.incrTwo()); threadTwo = new Thread(() -> syncTest.incrTwo()); thread.start(); threadTwo.start(); thread.join(); threadTwo.join(); //最終打印結(jié)果是40000 System.out.println(syncTest.getJ()); } }
先看看incr
同步方法執(zhí)行,流程和前面沒區(qū)別,只是Synchronized
鎖定的范圍太大,把syncDbData()
也納入臨界區(qū)中,多線程場景執(zhí)行,會有性能上的浪費(fèi),因?yàn)?code>syncDbData()完全可以讓多線程并行或并發(fā)執(zhí)行。
我們通過代碼塊的方式,來縮小范圍,定義正確的臨界區(qū),提升性能,目光轉(zhuǎn)到incrTwo
同步塊執(zhí)行,incrTwo
函數(shù)使用修飾代碼塊的方式同步,只對自增代碼段進(jìn)行鎖定。
代碼塊同步方式除了靈活控制范圍外,還能做線程間的協(xié)同工作,因?yàn)?code>Synchronized ()括號中能接收任何對象作為鎖,所以可以通過Object
的wait、notify、notifyAll
等函數(shù),做多線程間的通信協(xié)同(本文不對線程通信協(xié)同做展開,主角是Synchronized
,而且也不推薦去用這些方法,因?yàn)?code>LockSupport工具類會是更好的選擇)。
wait:當(dāng)前線程暫停,釋放鎖
notify:釋放鎖,喚醒調(diào)用了wait的線程(如果有多個(gè)隨機(jī)喚醒一個(gè))
notifyAll:釋放鎖,喚醒調(diào)用了wait的所有線程
public class SyncTest { private static int j = 0; /** * 同步庫數(shù)據(jù),比較耗時(shí),代碼資源不涉及共享資源讀寫操作。 */ public void syncDbData() { System.out.println("db數(shù)據(jù)開始同步------------"); try { //同步時(shí)間需要2秒 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("db數(shù)據(jù)開始同步完成------------"); } //自增方法 public synchronized void incr() { //start--臨界區(qū)代碼 //同步庫數(shù)據(jù) syncDbData(); for (int i = 0; i < 10000; i++) { j++; } //end--臨界區(qū)代碼 } //自增方法 public void incrTwo() { //同步庫數(shù)據(jù) syncDbData(); synchronized (this) { //start--臨界區(qū)代碼 for (int i = 0; i < 10000; i++) { j++; } //end--臨界區(qū)代碼 } } public int getJ() { return j; } }
為了探究Synchronized
原理,我們對上面的代碼進(jìn)行反編譯,輸出反編譯后結(jié)果,看看底層是如何實(shí)現(xiàn)的(環(huán)境Java 11、win 10系統(tǒng))。
只截取了incr與incrTwo函數(shù)內(nèi)容 public synchronized void incr(); Code: 0: aload_0 1: invokevirtual #11 // Method syncDbData:()V 4: iconst_0 5: istore_1 6: iload_1 7: sipush 10000 10: if_icmpge 27 13: getstatic #12 // Field j:I 16: iconst_1 17: iadd 18: putstatic #12 // Field j:I 21: iinc 1, 1 24: goto 6 27: return public void incrTwo(); Code: 0: aload_0 1: invokevirtual #11 // Method syncDbData:()V 4: aload_0 5: dup 6: astore_1 7: monitorenter //獲取鎖 8: iconst_0 9: istore_2 10: iload_2 11: sipush 10000 14: if_icmpge 31 17: getstatic #12 // Field j:I 20: iconst_1 21: iadd 22: putstatic #12 // Field j:I 25: iinc 2, 1 28: goto 10 31: aload_1 32: monitorexit //正常退出釋放鎖 33: goto 41 36: astore_3 37: aload_1 38: monitorexit //異步退出釋放鎖 39: aload_3 40: athrow 41: return
ps:對上面指令感興趣的讀者,可以百度或google一下“JVM 虛擬機(jī)字節(jié)碼指令表”
先看incrTwo
函數(shù),incrTwo
是代碼塊方式同步,在反編譯后的結(jié)果中,我們發(fā)現(xiàn)存在monitorenter
與monitorexit
指令(獲取鎖、釋放鎖)。
monitorenter
指令插入到同步代碼塊的開始位置,monitorexit
指令插入到同步代碼塊的結(jié)束位置,J V M
需要保證每一個(gè) monitorenter
都有monitorexit
與之對應(yīng)。
任何對象都有一個(gè)監(jiān)視器鎖(monitor
)關(guān)聯(lián),線程執(zhí)行monitorenter
指令時(shí)嘗試獲取monitor
的所有權(quán)。
如果monitor
的進(jìn)入數(shù)為0
,則該線程進(jìn)入monitor
,然后將進(jìn)入數(shù)設(shè)置為1
,該線程為monitor
的所有者
如果線程已經(jīng)占有該monitor
,重新進(jìn)入,則monitor
的進(jìn)入數(shù)加1
線程執(zhí)行monitorexit
,monitor
的進(jìn)入數(shù)-1,執(zhí)行過多少次monitorenter
,最終要執(zhí)行對應(yīng)次數(shù)的monitorexit
如果其他線程已經(jīng)占用monitor
,則該線程進(jìn)入阻塞狀態(tài),直到monitor
的進(jìn)入數(shù)為0,再重新嘗試獲取monitor
的所有權(quán)
回過頭看incr
函數(shù),incr
是普通函數(shù)方式同步,雖然在反編譯后的結(jié)果中沒有看到monitorenter
與monitorexit
指令,但是實(shí)際執(zhí)行的流程與incrTwo
函數(shù)一樣,通過monitor
來執(zhí)行,只不過它是一種隱式的方式來實(shí)現(xiàn),最后放一張流程圖。
Jdk 1.5
以后對Synchronized
關(guān)鍵字做了各種的優(yōu)化,經(jīng)過優(yōu)化后Synchronized
已經(jīng)變得越來越快了,這也是為什么官方建議使用Synchronized
的原因,具體的優(yōu)化點(diǎn)如下。
鎖粗化
鎖消除
鎖升級
互斥的臨界區(qū)范圍應(yīng)該盡可能小,這樣做的目的是為了使同步的操作數(shù)量盡可能縮小,縮短阻塞時(shí)間,如果存在鎖競爭,那么等待鎖的線程也能盡快拿到鎖。
但是加鎖解鎖也需要消耗資源,如果存在一系列的連續(xù)加鎖解鎖操作,可能會導(dǎo)致不必要的性能損耗,鎖粗化就是將「多個(gè)連續(xù)的加鎖、解鎖操作連接在一起」,擴(kuò)展成一個(gè)范圍更大的鎖,避免頻繁的加鎖解鎖操作。
J V M
會檢測到一連串的操作都對同一個(gè)對象加鎖(for
循環(huán)10000
次執(zhí)行j++
,沒有鎖粗化就要進(jìn)行10000
次加鎖/解鎖),此時(shí)J V M
就會將加鎖的范圍粗化到這一連串操作的外部(比如for
循環(huán)體外),使得這一連串操作只需要加一次鎖即可。
Java
虛擬機(jī)在JIT
編譯時(shí)(可以簡單理解為當(dāng)某段代碼即將第一次被執(zhí)行時(shí)進(jìn)行編譯,又稱即時(shí)編譯),通過對運(yùn)行上下文的掃描,經(jīng)過逃逸分析(對象在函數(shù)中被使用,也可能被外部函數(shù)所引用,稱為函數(shù)逃逸),去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節(jié)省毫無意義的時(shí)間消耗。
代碼中使用Object
作為鎖,但是Object
對象的生命周期只在incrFour()
函數(shù)中,并不會被其他線程所訪問到,所以在J I T
編譯階段就會被優(yōu)化掉(此處的Object
屬于沒有逃逸的對象)。
Java
中每個(gè)對象都擁有對象頭,對象頭由Mark World
、指向類的指針、以及數(shù)組長度三部分組成,本文,我們只需要關(guān)心Mark World
即可, Mark World
記錄了對象的HashCode
、分代年齡和鎖標(biāo)志位信息。
Mark World簡化結(jié)構(gòu)
鎖狀態(tài) | 存儲內(nèi)容 | 鎖標(biāo)記 |
---|---|---|
無鎖 | 對象的hashCode、對象分代年齡、是否是偏向鎖(0) | 01 |
偏向鎖 | 偏向線程ID、偏向時(shí)間戳、對象分代年齡、是否是偏向鎖(1) | 01 |
輕量級鎖 | 指向棧中鎖記錄的指針 | 00 |
重量級鎖 | 指向互斥量(重量級鎖)的指針 | 10 |
讀者們只需知道,鎖的升級變化,體現(xiàn)在鎖對象的對象頭Mark World
部分,也就是說Mark World
的內(nèi)容會隨著鎖升級而改變。
Java1.5
以后為了減少獲取鎖和釋放鎖帶來的性能消耗,引入了偏向鎖和輕量級鎖,Synchronized
的升級順序是 「無鎖-->偏向鎖-->輕量級鎖-->重量級鎖,只會升級不會降級」
在大多數(shù)情況下,鎖總是由同一線程多次獲得,不存在多線程競爭,所以出現(xiàn)了偏向鎖,其目標(biāo)就是在只有一個(gè)線程執(zhí)行同步代碼塊時(shí),降低獲取鎖帶來的消耗,提高性能(可以通過J V M參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false,關(guān)閉之后程序默認(rèn)會進(jìn)入輕量級鎖狀態(tài))。
線程執(zhí)行同步代碼或方法前,線程只需要判斷對象頭的Mark Word
中線程ID
與當(dāng)前線程ID
是否一致,如果一致直接執(zhí)行同步代碼或方法,具體流程如下
無鎖狀態(tài),存儲內(nèi)容「是否為偏向鎖(0
)」,鎖標(biāo)識位01
CAS
設(shè)置當(dāng)前線程ID到Mark Word
存儲內(nèi)容中
是否為偏向鎖0
=> 是否為偏向鎖1
執(zhí)行同步代碼或方法
偏向鎖狀態(tài),存儲內(nèi)容「是否為偏向鎖(1
)、線程ID」,鎖標(biāo)識位01
對比線程ID
是否一致,如果一致執(zhí)行同步代碼或方法,否則進(jìn)入下面的流程
如果不一致,CAS
將Mark Word
的線程ID
設(shè)置為當(dāng)前線程ID
,設(shè)置成功,執(zhí)行同步代碼或方法,否則進(jìn)入下面的流程
CAS
設(shè)置失敗,證明存在多線程競爭情況,觸發(fā)撤銷偏向鎖,當(dāng)?shù)竭_(dá)全局安全點(diǎn),偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然后在安全點(diǎn)的位置恢復(fù)繼續(xù)往下執(zhí)行。
輕量級鎖考慮的是競爭鎖對象的線程不多,持有鎖時(shí)間也不長的場景。因?yàn)樽枞€程需要C P U
從用戶態(tài)轉(zhuǎn)到內(nèi)核態(tài),代價(jià)較大,如果剛剛阻塞不久這個(gè)鎖就被釋放了,那這個(gè)代價(jià)就有點(diǎn)得不償失,所以干脆不阻塞這個(gè)線程,讓它自旋一段時(shí)間等待鎖釋放。
當(dāng)前線程持有的鎖是偏向鎖的時(shí)候,被另外的線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。 輕量級鎖的獲取主要有兩種情況:① 當(dāng)關(guān)閉偏向鎖功能時(shí);② 多個(gè)線程競爭偏向鎖導(dǎo)致偏向鎖升級為輕量級鎖。
無鎖狀態(tài),存儲內(nèi)容「是否為偏向鎖(0
)」,鎖標(biāo)識位01
關(guān)閉偏向鎖功能時(shí)
CAS
設(shè)置當(dāng)前線程棧中鎖記錄的指針到Mark Word
存儲內(nèi)容
鎖標(biāo)識位設(shè)置為00
執(zhí)行同步代碼或方法
釋放鎖時(shí),還原來Mark Word
內(nèi)容
輕量級鎖狀態(tài),存儲內(nèi)容「線程棧中鎖記錄的指針」,鎖標(biāo)識位00
(存儲內(nèi)容的線程是指"持有輕量級鎖的線程")
CAS
設(shè)置當(dāng)前線程棧中鎖記錄的指針到Mark Word
存儲內(nèi)容,設(shè)置成功獲取輕量級鎖,執(zhí)行同步塊代碼或方法,否則執(zhí)行下面的邏輯
設(shè)置失敗,證明多線程存在一定競爭,線程自旋上一步的操作,自旋一定次數(shù)后還是失敗,輕量級鎖升級為重量級鎖
Mark Word
存儲內(nèi)容替換成重量級鎖指針,鎖標(biāo)記位10
輕量級鎖膨脹之后,就升級為重量級鎖,重量級鎖是依賴操作系統(tǒng)的MutexLock
(互斥鎖)來實(shí)現(xiàn)的,需要從用戶態(tài)轉(zhuǎn)到內(nèi)核態(tài),這個(gè)成本非常高,這就是為什么Java1.6
之前Synchronized
效率低的原因。
升級為重量級鎖時(shí),鎖標(biāo)志位的狀態(tài)值變?yōu)?code>10,此時(shí)Mark Word
中存儲內(nèi)容的是重量級鎖的指針,等待鎖的線程都會進(jìn)入阻塞狀態(tài),下面是簡化版的鎖升級過程。
到此,相信大家對“java中Synchronized的原理及應(yīng)用”有了更深的了解,不妨來實(shí)際操作一番吧!這里是創(chuàng)新互聯(lián)網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!